diff options
Diffstat (limited to 'dom/quota/ActorsParent.cpp')
-rw-r--r-- | dom/quota/ActorsParent.cpp | 11180 |
1 files changed, 11180 insertions, 0 deletions
diff --git a/dom/quota/ActorsParent.cpp b/dom/quota/ActorsParent.cpp new file mode 100644 index 0000000000..09de6255bf --- /dev/null +++ b/dom/quota/ActorsParent.cpp @@ -0,0 +1,11180 @@ +/* -*- 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 "InitializationTypes.h" +#include "OriginScope.h" +#include "QuotaCommon.h" +#include "QuotaManager.h" +#include "QuotaObject.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 "ErrorList.h" +#include "GeckoProfiler.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/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/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/FlippedOnce.h" +#include "mozilla/dom/LocalStorageCommon.h" +#include "mozilla/dom/Nullable.h" +#include "mozilla/dom/StorageActivityService.h" +#include "mozilla/dom/StorageDBUpdater.h" +#include "mozilla/dom/StorageTypeBinding.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/CheckedUnsafePtr.h" +#include "mozilla/dom/quota/Client.h" +#include "mozilla/dom/quota/PersistenceType.h" +#include "mozilla/dom/quota/PQuota.h" +#include "mozilla/dom/quota/PQuotaParent.h" +#include "mozilla/dom/quota/PQuotaRequest.h" +#include "mozilla/dom/quota/PQuotaRequestParent.h" +#include "mozilla/dom/quota/PQuotaUsageRequest.h" +#include "mozilla/dom/quota/PQuotaUsageRequestParent.h" +#include "mozilla/dom/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/MozURL.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsBaseHashtable.h" +#include "nsCOMPtr.h" +#include "nsCRTGlue.h" +#include "nsCharSeparatedTokenizer.h" +#include "nsClassHashtable.h" +#include "nsComponentManagerUtils.h" +#include "nsContentUtils.h" +#include "nsDataHashtable.h" +#include "nsDebug.h" +#include "nsDirectoryServiceUtils.h" +#include "nsError.h" +#include "nsHashKeys.h" +#include "nsIBinaryInputStream.h" +#include "nsIBinaryOutputStream.h" +#include "nsIConsoleService.h" +#include "nsIDirectoryEnumerator.h" +#include "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 "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 "nsServiceManagerUtils.h" +#include "nsString.h" +#include "nsStringFlags.h" +#include "nsStringFwd.h" +#include "nsTArray.h" +#include "nsTHashtable.h" +#include "nsTLiteralString.h" +#include "nsTPromiseFlatString.h" +#include "nsTStringRepr.h" +#include "nsThreadUtils.h" +#include "nsURLHelper.h" +#include "nsXPCOM.h" +#include "nsXPCOMCID.h" +#include "nsXULAppAPI.h" +#include "prinrval.h" +#include "prio.h" +#include "prthread.h" +#include "prtime.h" + +#define DISABLE_ASSERTS_FOR_FUZZING 0 + +#if DISABLE_ASSERTS_FOR_FUZZING +# define ASSERT_UNLESS_FUZZING(...) \ + do { \ + } while (0) +#else +# define ASSERT_UNLESS_FUZZING(...) MOZ_ASSERT(false, __VA_ARGS__) +#endif + +// As part of bug 1536596 in order to identify the remaining sources of +// principal info inconsistencies, we have added anonymized crash logging and +// are temporarily making these checks occur on both debug and optimized +// nightly, dev-edition, and early beta builds through use of +// EARLY_BETA_OR_EARLIER during Firefox 82. The plan is to return this +// condition to MOZ_DIAGNOSTIC_ASSERT_ENABLED during Firefox 84 at the latest. +// The analysis and disabling is tracked by bug 1536596. + +#ifdef EARLY_BETA_OR_EARLIER +# define QM_PRINCIPALINFO_VERIFICATION_ENABLED +#endif + +#define QM_LOG_TEST() MOZ_LOG_TEST(GetQuotaManagerLogger(), LogLevel::Info) +#define QM_LOG(_args) MOZ_LOG(GetQuotaManagerLogger(), LogLevel::Info, _args) + +// 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_FORCE_KILL_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_FORCE_CRASH_TIMEOUT_MS 45000 + +// 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; +using mozilla::net::MozURL; + +// 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 "/:*?\"<>|\\"; + +namespace { + +template <typename T> +void AssertNoOverflow(uint64_t aDest, T aArg); + +/******************************************************************************* + * 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); + +static_assert(static_cast<uint32_t>(StorageType::Persistent) == + static_cast<uint32_t>(PERSISTENCE_TYPE_PERSISTENT), + "Enum values should match."); + +static_assert(static_cast<uint32_t>(StorageType::Temporary) == + static_cast<uint32_t>(PERSISTENCE_TYPE_TEMPORARY), + "Enum values should match."); + +static_assert(static_cast<uint32_t>(StorageType::Default) == + static_cast<uint32_t>(PERSISTENCE_TYPE_DEFAULT), + "Enum values should match."); + +const char kChromeOrigin[] = "chrome"; +const char kAboutHomeOriginPrefix[] = "moz-safe-about:home"; +const char kIndexedDBOriginPrefix[] = "indexeddb://"; +const char kResourceOriginPrefix[] = "resource://"; + +constexpr auto kPersistentOriginTelemetryKey = "PersistentOrigin"_ns; +constexpr auto kTemporaryOriginTelemetryKey = "TemporaryOrigin"_ns; + +constexpr auto kStorageName = u"storage"_ns; +constexpr auto kSQLiteSuffix = u".sqlite"_ns; + +#define INDEXEDDB_DIRECTORY_NAME u"indexedDB" +#define PERSISTENT_DIRECTORY_NAME u"persistent" +#define PERMANENT_DIRECTORY_NAME u"permanent" +#define TEMPORARY_DIRECTORY_NAME u"temporary" +#define DEFAULT_DIRECTORY_NAME u"default" + +// The name of the file that we use to load/save the last access time of an +// origin. +// XXX We should get rid of old metadata files at some point, bug 1343576. +#define METADATA_FILE_NAME u".metadata" +#define METADATA_TMP_FILE_NAME u".metadata-tmp" +#define METADATA_V2_FILE_NAME u".metadata-v2" +#define METADATA_V2_TMP_FILE_NAME u".metadata-v2-tmp" + +#define WEB_APPS_STORE_FILE_NAME u"webappsstore.sqlite" +#define LS_ARCHIVE_FILE_NAME u"ls-archive.sqlite" +#define LS_ARCHIVE_TMP_FILE_NAME u"ls-archive-tmp.sqlite" + +const uint32_t kLocalStorageArchiveVersion = 4; + +const char kProfileDoChangeTopic[] = "profile-do-change"; + +const int32_t kCacheVersion = 1; + +/****************************************************************************** + * 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( + 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(aConnection, GetSchemaVersion)); + + MOZ_ASSERT(storageVersion == 0); + } +#endif + + QM_TRY(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(stmt, GetInt32, 0)); +} + +nsresult SaveCacheVersion(mozIStorageConnection* aConnection, + int32_t aVersion) { + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + QM_TRY_INSPECT( + const auto& stmt, + MOZ_TO_RESULT_INVOKE_TYPED( + nsCOMPtr<mozIStorageStatement>, aConnection, CreateStatement, + "UPDATE database SET cache_version = :version;"_ns)); + + QM_TRY(stmt->BindInt32ByName("version"_ns, aVersion)); + + QM_TRY(stmt->Execute()); + + return NS_OK; +} + +nsresult CreateCacheTables(mozIStorageConnection* aConnection) { + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + // Table `cache` + QM_TRY( + aConnection->ExecuteSimpleSQL("CREATE TABLE cache" + "( valid INTEGER NOT NULL DEFAULT 0" + ", build_id TEXT NOT NULL DEFAULT ''" + ");"_ns)); + + // Table `repository` + QM_TRY( + aConnection->ExecuteSimpleSQL("CREATE TABLE repository" + "( id INTEGER PRIMARY KEY" + ", name TEXT NOT NULL" + ");"_ns)); + + // Table `origin` + QM_TRY( + aConnection->ExecuteSimpleSQL("CREATE TABLE origin" + "( repository_id INTEGER NOT NULL" + ", origin TEXT NOT NULL" + ", group_ 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(SaveCacheVersion(aConnection, kCacheVersion)); + + return NS_OK; +} + +/* +nsresult UpgradeCacheFrom1To2(mozIStorageConnection* aConnection) { + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + nsresult rv; + +#ifdef DEBUG + { + int32_t cacheVersion; + rv = LoadCacheVersion(aConnection, cacheVersion); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(cacheVersion == 1); + } +#endif + + rv = SaveCacheVersion(aConnection, 2); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} +*/ + +nsresult InvalidateCache(mozIStorageConnection& aConnection) { + AssertIsOnIOThread(); + + mozStorageTransaction transaction( + &aConnection, false, mozIStorageConnection::TRANSACTION_IMMEDIATE); + + QM_TRY(aConnection.ExecuteSimpleSQL("DELETE FROM origin;"_ns)); + QM_TRY(aConnection.ExecuteSimpleSQL("UPDATE cache SET valid = 0"_ns)); + QM_TRY(transaction.Commit()); + + return NS_OK; +} + +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(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(aWebAppsStoreFile, IsDirectory)); + + if (isDirectory) { + QM_WARNING("webappsstore.sqlite is not a file!"); + return nsCOMPtr<mozIStorageConnection>{}; + } + + QM_TRY_INSPECT( + const auto& connection, + MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr<mozIStorageConnection>, + aStorageService, OpenUnsharedDatabase, + &aWebAppsStoreFile) + .orElse([](const nsresult rv) + -> Result<nsCOMPtr<mozIStorageConnection>, nsresult> { + if (rv == NS_ERROR_FILE_CORRUPTED) { + // Don't throw an error, leave a corrupted webappsstore database + // as it is. + return nsCOMPtr<mozIStorageConnection>{}; + } + + return Err(rv); + })); + + if (connection) { + // Don't propagate an error, leave a non-updateable webappsstore database as + // it is. + QM_TRY(StorageDBUpdater::Update(connection), + nsCOMPtr<mozIStorageConnection>{}); + } + + return connection; +} + +Result<nsCOMPtr<nsIFile>, nsresult> GetLocalStorageArchiveFile( + const nsAString& aDirectoryPath) { + AssertIsOnIOThread(); + MOZ_ASSERT(!aDirectoryPath.IsEmpty()); + + QM_TRY_UNWRAP(auto lsArchiveFile, QM_NewLocalFile(aDirectoryPath)); + + QM_TRY(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(lsArchiveTmpFile->Append(nsLiteralString(LS_ARCHIVE_TMP_FILE_NAME))); + + return lsArchiveTmpFile; +} + +nsresult InitializeLocalStorageArchive(mozIStorageConnection* aConnection, + uint32_t aVersion) { + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + QM_TRY(aConnection->ExecuteSimpleSQL( + "CREATE TABLE database(version INTEGER NOT NULL DEFAULT 0);"_ns)); + + QM_TRY_INSPECT( + const auto& stmt, + MOZ_TO_RESULT_INVOKE_TYPED( + nsCOMPtr<mozIStorageStatement>, aConnection, CreateStatement, + "INSERT INTO database (version) VALUES (:version)"_ns)); + + QM_TRY(stmt->BindInt32ByName("version"_ns, aVersion)); + QM_TRY(stmt->Execute()); + + return NS_OK; +} + +Result<bool, nsresult> IsLocalStorageArchiveInitialized( + mozIStorageConnection& aConnection) { + AssertIsOnIOThread(); + + QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE(aConnection, TableExists, "database"_ns)); +} + +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(stmt, GetInt32, 0)); +} + +/* +nsresult SaveLocalStorageArchiveVersion(mozIStorageConnection* aConnection, + uint32_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 bool& isDirectory, + MOZ_TO_RESULT_INVOKE(file, IsDirectory)); + + return isDirectory ? aDirectoryFunc(file) : aFileFunc(file); + }); +} + +/****************************************************************************** + * Quota manager class declarations + ******************************************************************************/ + +} // namespace + +enum class ShouldUpdateLockIdTableFlag { No, Yes }; + +class DirectoryLockImpl final : public DirectoryLock { + const NotNull<RefPtr<QuotaManager>> mQuotaManager; + + const Nullable<PersistenceType> mPersistenceType; + const nsCString mGroup; + const OriginScope mOriginScope; + const Nullable<Client::Type> mClientType; + LazyInitializedOnceEarlyDestructible< + const NotNull<RefPtr<OpenDirectoryListener>>> + mOpenListener; + + nsTArray<NotNull<DirectoryLockImpl*>> mBlocking; + nsTArray<NotNull<DirectoryLockImpl*>> mBlockedOn; + + const int64_t mId; + + const bool mExclusive; + + // Internal quota manager operations use this flag to prevent directory lock + // registraction/unregistration from updating origin access time, etc. + const bool mInternal; + + const bool mShouldUpdateLockIdTable; + + bool mRegistered; + FlippedOnce<true> mPending; + FlippedOnce<false> mInvalidated; + + public: + DirectoryLockImpl(MovingNotNull<RefPtr<QuotaManager>> aQuotaManager, + const int64_t aId, + const Nullable<PersistenceType>& aPersistenceType, + const nsACString& aGroup, const OriginScope& aOriginScope, + const Nullable<Client::Type>& aClientType, bool aExclusive, + bool aInternal, + ShouldUpdateLockIdTableFlag aShouldUpdateLockIdTableFlag, + RefPtr<OpenDirectoryListener> aOpenListener); + + void AssertIsOnOwningThread() const +#ifdef DEBUG + ; +#else + { + } +#endif + + const Nullable<PersistenceType>& NullablePersistenceType() const { + return mPersistenceType; + } + + const OriginScope& GetOriginScope() const { return mOriginScope; } + + const Nullable<Client::Type>& NullableClientType() const { + return mClientType; + } + + bool IsInternal() const { return mInternal; } + + void SetRegistered(bool aRegistered) { mRegistered = aRegistered; } + + bool IsPending() const { return mPending; } + + // Ideally, we would have just one table (instead of these two: + // QuotaManager::mDirectoryLocks and QuotaManager::mDirectoryLockIdTable) for + // all registered locks. However, some directory locks need to be accessed off + // the PBackground thread, so the access must be protected by the quota mutex. + // The problem is that directory locks for eviction must be currently created + // while the mutex lock is already acquired. So we decided to have two tables + // for now and to not register directory locks for eviction in + // QuotaMnaager::mDirectoryLockIdTable. This can be improved in future after + // some refactoring of the mutex locking. + bool ShouldUpdateLockIdTable() const { return mShouldUpdateLockIdTable; } + + bool ShouldUpdateLockTable() { + return !mInternal && + mPersistenceType.Value() != PERSISTENCE_TYPE_PERSISTENT; + } + + bool Overlaps(const DirectoryLockImpl& aLock) const; + + // Test whether this DirectoryLock needs to wait for the given lock. + bool MustWaitFor(const DirectoryLockImpl& aLock) const; + + void AddBlockingLock(DirectoryLockImpl& aLock) { + AssertIsOnOwningThread(); + + mBlocking.AppendElement(WrapNotNull(&aLock)); + } + + const nsTArray<NotNull<DirectoryLockImpl*>>& GetBlockedOnLocks() { + return mBlockedOn; + } + + void AddBlockedOnLock(DirectoryLockImpl& aLock) { + AssertIsOnOwningThread(); + + mBlockedOn.AppendElement(WrapNotNull(&aLock)); + } + + void MaybeUnblock(DirectoryLockImpl& aLock) { + AssertIsOnOwningThread(); + + mBlockedOn.RemoveElement(&aLock); + if (mBlockedOn.IsEmpty()) { + NotifyOpenListener(); + } + } + + void NotifyOpenListener(); + + void Invalidate() { + AssertIsOnOwningThread(); + + mInvalidated.EnsureFlipped(); + } + + NS_INLINE_DECL_REFCOUNTING(DirectoryLockImpl, override) + + int64_t Id() const { return mId; } + + PersistenceType GetPersistenceType() const { + MOZ_DIAGNOSTIC_ASSERT(!mPersistenceType.IsNull()); + + return mPersistenceType.Value(); + } + + quota::GroupAndOrigin GroupAndOrigin() const { + MOZ_DIAGNOSTIC_ASSERT(!mGroup.IsEmpty()); + + return quota::GroupAndOrigin{mGroup, nsCString(Origin())}; + } + + const nsACString& Origin() const { + MOZ_DIAGNOSTIC_ASSERT(mOriginScope.IsOrigin()); + MOZ_DIAGNOSTIC_ASSERT(!mOriginScope.GetOrigin().IsEmpty()); + + return mOriginScope.GetOrigin(); + } + + Client::Type ClientType() const { + MOZ_DIAGNOSTIC_ASSERT(!mClientType.IsNull()); + MOZ_DIAGNOSTIC_ASSERT(mClientType.Value() < Client::TypeMax()); + + return mClientType.Value(); + } + + already_AddRefed<DirectoryLock> Specialize( + PersistenceType aPersistenceType, + const quota::GroupAndOrigin& aGroupAndOrigin, + Client::Type aClientType) const; + + void Log() const; + + private: + ~DirectoryLockImpl(); +}; + +const DirectoryLockImpl* GetDirectoryLockImpl( + const DirectoryLock* aDirectoryLock) { + MOZ_ASSERT(aDirectoryLock); + + return static_cast<const DirectoryLockImpl*>(aDirectoryLock); +} + +class QuotaManager::Observer final : public nsIObserver { + static Observer* sInstance; + + bool mPendingProfileChange; + bool mShutdownComplete; + + public: + static nsresult Initialize(); + + 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 + +// XXX Change this not to derive from AutoTArray. +class ClientUsageArray final + : public AutoTArray<Maybe<uint64_t>, Client::TYPE_MAX> { + public: + ClientUsageArray() { SetLength(Client::TypeMax()); } + + void Serialize(nsACString& aText) const; + + nsresult Deserialize(const nsACString& aText); + + ClientUsageArray Clone() const { + ClientUsageArray res; + res.Assign(*this); + return res; + } +}; + +class OriginInfo final { + friend class GroupInfo; + friend class QuotaManager; + friend class QuotaObject; + + public: + OriginInfo(GroupInfo* aGroupInfo, const nsACString& aOrigin, + const ClientUsageArray& aClientUsages, uint64_t aUsage, + int64_t aAccessTime, bool aPersisted, bool aDirectoryExists); + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(OriginInfo) + + GroupInfo* GetGroupInfo() const { return mGroupInfo; } + + const nsCString& Origin() const { return mOrigin; } + + int64_t LockedUsage() const { + AssertCurrentThreadOwnsQuotaMutex(); + +#ifdef DEBUG + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + uint64_t usage = 0; + for (Client::Type type : quotaManager->AllClientTypes()) { + AssertNoOverflow(usage, mClientUsages[type].valueOr(0)); + usage += mClientUsages[type].valueOr(0); + } + MOZ_ASSERT(mUsage == usage); +#endif + + return mUsage; + } + + int64_t LockedAccessTime() const { + AssertCurrentThreadOwnsQuotaMutex(); + + return mAccessTime; + } + + bool LockedPersisted() const { + AssertCurrentThreadOwnsQuotaMutex(); + + return mPersisted; + } + + nsresult LockedBindToStatement(mozIStorageStatement* aStatement) const; + + private: + // Private destructor, to discourage deletion outside of Release(): + ~OriginInfo() { + MOZ_COUNT_DTOR(OriginInfo); + + MOZ_ASSERT(!mQuotaObjects.Count()); + } + + void LockedDecreaseUsage(Client::Type aClientType, int64_t aSize); + + void LockedResetUsageForClient(Client::Type aClientType); + + UsageInfo LockedGetUsageForClient(Client::Type aClientType); + + void LockedUpdateAccessTime(int64_t aAccessTime) { + AssertCurrentThreadOwnsQuotaMutex(); + + mAccessTime = aAccessTime; + if (!mAccessed) { + mAccessed = true; + } + } + + void LockedPersist(); + + nsDataHashtable<nsStringHashKey, QuotaObject*> mQuotaObjects; + ClientUsageArray mClientUsages; + GroupInfo* mGroupInfo; + const nsCString mOrigin; + uint64_t mUsage; + int64_t mAccessTime; + bool mAccessed; + bool mPersisted; + /** + * In some special cases like the LocalStorage client where it's possible to + * create a Quota-using representation but not actually write any data, we + * want to be able to track quota for an origin without creating its origin + * directory or the per-client files until they are actually needed to store + * data. In those cases, the OriginInfo will be created by + * EnsureQuotaForOrigin and the resulting mDirectoryExists will be false until + * the origin actually needs to be created. It is possible for mUsage to be + * greater than zero while mDirectoryExists is false, representing a state + * where a client like LocalStorage has reserved quota for disk writes, but + * has not yet flushed the data to disk. + */ + bool mDirectoryExists; +}; + +class OriginInfoLRUComparator { + public: + bool Equals(const NotNull<RefPtr<OriginInfo>>& a, + const NotNull<RefPtr<OriginInfo>>& b) const { + return a->LockedAccessTime() == b->LockedAccessTime(); + } + + bool LessThan(const NotNull<RefPtr<OriginInfo>>& a, + const NotNull<RefPtr<OriginInfo>>& b) const { + return a->LockedAccessTime() < b->LockedAccessTime(); + } +}; + +class GroupInfo final { + friend class GroupInfoPair; + friend class OriginInfo; + friend class QuotaManager; + friend class QuotaObject; + + public: + GroupInfo(GroupInfoPair* aGroupInfoPair, PersistenceType aPersistenceType, + const nsACString& aGroup) + : mGroupInfoPair(aGroupInfoPair), + mPersistenceType(aPersistenceType), + mGroup(aGroup), + mUsage(0) { + MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT); + + MOZ_COUNT_CTOR(GroupInfo); + } + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GroupInfo) + + PersistenceType GetPersistenceType() const { return mPersistenceType; } + + private: + // Private destructor, to discourage deletion outside of Release(): + MOZ_COUNTED_DTOR(GroupInfo) + + already_AddRefed<OriginInfo> LockedGetOriginInfo(const nsACString& aOrigin); + + void LockedAddOriginInfo(NotNull<RefPtr<OriginInfo>>&& aOriginInfo); + + void LockedAdjustUsageForRemovedOriginInfo(const OriginInfo& aOriginInfo); + + void LockedRemoveOriginInfo(const nsACString& aOrigin); + + void LockedRemoveOriginInfos(); + + bool LockedHasOriginInfos() { + AssertCurrentThreadOwnsQuotaMutex(); + + return !mOriginInfos.IsEmpty(); + } + + nsTArray<NotNull<RefPtr<OriginInfo>>> mOriginInfos; + + GroupInfoPair* mGroupInfoPair; + PersistenceType mPersistenceType; + nsCString mGroup; + uint64_t mUsage; +}; + +class GroupInfoPair { + friend class QuotaManager; + friend class QuotaObject; + + public: + MOZ_COUNTED_DEFAULT_CTOR(GroupInfoPair) + + MOZ_COUNTED_DTOR(GroupInfoPair) + + private: + already_AddRefed<GroupInfo> LockedGetGroupInfo( + PersistenceType aPersistenceType) { + AssertCurrentThreadOwnsQuotaMutex(); + MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT); + + RefPtr<GroupInfo> groupInfo = + GetGroupInfoForPersistenceType(aPersistenceType); + return groupInfo.forget(); + } + + void LockedSetGroupInfo(PersistenceType aPersistenceType, + GroupInfo* aGroupInfo) { + AssertCurrentThreadOwnsQuotaMutex(); + MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT); + + RefPtr<GroupInfo>& groupInfo = + GetGroupInfoForPersistenceType(aPersistenceType); + groupInfo = aGroupInfo; + } + + void LockedClearGroupInfo(PersistenceType aPersistenceType) { + AssertCurrentThreadOwnsQuotaMutex(); + MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT); + + RefPtr<GroupInfo>& groupInfo = + GetGroupInfoForPersistenceType(aPersistenceType); + groupInfo = nullptr; + } + + bool LockedHasGroupInfos() { + AssertCurrentThreadOwnsQuotaMutex(); + + return mTemporaryStorageGroupInfo || mDefaultStorageGroupInfo; + } + + RefPtr<GroupInfo>& GetGroupInfoForPersistenceType( + PersistenceType aPersistenceType); + + RefPtr<GroupInfo> mTemporaryStorageGroupInfo; + RefPtr<GroupInfo> mDefaultStorageGroupInfo; +}; + +namespace { + +class CollectOriginsHelper final : public Runnable { + uint64_t mMinSizeToBeFreed; + + Mutex& mMutex; + CondVar mCondVar; + + // The members below are protected by mMutex. + nsTArray<RefPtr<DirectoryLockImpl>> 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<DirectoryLockImpl>>& aLocks); + + private: + ~CollectOriginsHelper() = default; + + NS_IMETHOD + Run() override; +}; + +class OriginOperationBase : public BackgroundThreadObject, public Runnable { + protected: + nsresult mResultCode; + + enum State { + // Not yet run. + State_Initial, + + // Running quota manager initialization on the owning thread. + State_CreatingQuotaManager, + + // Running on the owning thread in the listener for OpenDirectory. + State_DirectoryOpenPending, + + // Running on the IO thread. + State_DirectoryWorkOpen, + + // Running on the owning thread after all work is done. + State_UnblockingOpen, + + // All done. + State_Complete + }; + + private: + State mState; + bool mActorDestroyed; + + protected: + bool mNeedsQuotaManagerInit; + bool mNeedsStorageInit; + + public: + void NoteActorDestroyed() { + AssertIsOnOwningThread(); + + mActorDestroyed = true; + } + + bool IsActorDestroyed() const { + AssertIsOnOwningThread(); + + return mActorDestroyed; + } + + protected: + explicit OriginOperationBase( + nsIEventTarget* aOwningThread = GetCurrentEventTarget()) + : BackgroundThreadObject(aOwningThread), + Runnable("dom::quota::OriginOperationBase"), + mResultCode(NS_OK), + mState(State_Initial), + mActorDestroyed(false), + mNeedsQuotaManagerInit(false), + mNeedsStorageInit(false) {} + + // Reference counted. + virtual ~OriginOperationBase() { + MOZ_ASSERT(mState == State_Complete); + MOZ_ASSERT(mActorDestroyed); + } + +#ifdef DEBUG + State GetState() const { return mState; } +#endif + + void SetState(State aState) { + MOZ_ASSERT(mState == State_Initial); + mState = aState; + } + + void AdvanceState() { + switch (mState) { + case State_Initial: + mState = State_CreatingQuotaManager; + return; + case State_CreatingQuotaManager: + mState = State_DirectoryOpenPending; + return; + case State_DirectoryOpenPending: + mState = State_DirectoryWorkOpen; + return; + case State_DirectoryWorkOpen: + mState = State_UnblockingOpen; + return; + case State_UnblockingOpen: + mState = State_Complete; + return; + default: + MOZ_CRASH("Bad state!"); + } + } + + NS_IMETHOD + Run() override; + + virtual void Open() = 0; + + nsresult DirectoryOpen(); + + virtual nsresult DoDirectoryWork(QuotaManager& aQuotaManager) = 0; + + void Finish(nsresult aResult); + + virtual void UnblockOpen() = 0; + + private: + nsresult Init(); + + nsresult FinishInit(); + + nsresult QuotaManagerOpen(); + + nsresult DirectoryWork(); +}; + +class FinalizeOriginEvictionOp : public OriginOperationBase { + nsTArray<RefPtr<DirectoryLockImpl>> mLocks; + + public: + FinalizeOriginEvictionOp(nsIEventTarget* aBackgroundThread, + nsTArray<RefPtr<DirectoryLockImpl>>&& aLocks) + : OriginOperationBase(aBackgroundThread), mLocks(std::move(aLocks)) { + MOZ_ASSERT(!NS_IsMainThread()); + } + + void Dispatch(); + + void RunOnIOThreadImmediately(); + + private: + ~FinalizeOriginEvictionOp() = default; + + virtual void Open() override; + + virtual nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + virtual void UnblockOpen() override; +}; + +class NormalOriginOperationBase + : public OriginOperationBase, + public OpenDirectoryListener, + public SupportsCheckedUnsafePtr<CheckIf<DiagnosticAssertEnabled>> { + RefPtr<DirectoryLock> mDirectoryLock; + + protected: + Nullable<PersistenceType> mPersistenceType; + OriginScope mOriginScope; + Nullable<Client::Type> mClientType; + mozilla::Atomic<bool> mCanceled; + const bool mExclusive; + bool mNeedsDirectoryLocking; + + public: + void RunImmediately() { + MOZ_ASSERT(GetState() == State_Initial); + + MOZ_ALWAYS_SUCCEEDS(this->Run()); + } + + protected: + NormalOriginOperationBase(const Nullable<PersistenceType>& aPersistenceType, + const OriginScope& aOriginScope, bool aExclusive) + : mPersistenceType(aPersistenceType), + mOriginScope(aOriginScope), + mExclusive(aExclusive), + mNeedsDirectoryLocking(true) { + AssertIsOnOwningThread(); + } + + ~NormalOriginOperationBase() = default; + + private: + // Need to declare refcounting unconditionally, because + // OpenDirectoryListener has pure-virtual refcounting. + NS_DECL_ISUPPORTS_INHERITED + + virtual void Open() override; + + virtual void UnblockOpen() override; + + // OpenDirectoryListener overrides. + virtual void DirectoryLockAcquired(DirectoryLock* aLock) override; + + virtual void DirectoryLockFailed() override; + + // Used to send results before unblocking open. + virtual void SendResults() = 0; +}; + +class SaveOriginAccessTimeOp : public NormalOriginOperationBase { + int64_t mTimestamp; + + public: + SaveOriginAccessTimeOp(PersistenceType aPersistenceType, + const nsACString& aOrigin, int64_t aTimestamp) + : NormalOriginOperationBase(Nullable<PersistenceType>(aPersistenceType), + OriginScope::FromOrigin(aOrigin), + /* aExclusive */ false), + mTimestamp(aTimestamp) { + AssertIsOnOwningThread(); + } + + private: + ~SaveOriginAccessTimeOp() = default; + + virtual nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + virtual void SendResults() override; +}; + +/******************************************************************************* + * Actor class declarations + ******************************************************************************/ + +class Quota final : public PQuotaParent { +#ifdef DEBUG + bool mActorDestroyed; +#endif + + public: + Quota(); + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(mozilla::dom::quota::Quota) + + private: + ~Quota(); + + void StartIdleMaintenance(); + + bool VerifyRequestParams(const UsageRequestParams& aParams) const; + + bool VerifyRequestParams(const RequestParams& aParams) const; + + // IPDL methods. + virtual void ActorDestroy(ActorDestroyReason aWhy) override; + + virtual PQuotaUsageRequestParent* AllocPQuotaUsageRequestParent( + const UsageRequestParams& aParams) override; + + virtual mozilla::ipc::IPCResult RecvPQuotaUsageRequestConstructor( + PQuotaUsageRequestParent* aActor, + const UsageRequestParams& aParams) override; + + virtual bool DeallocPQuotaUsageRequestParent( + PQuotaUsageRequestParent* aActor) override; + + virtual PQuotaRequestParent* AllocPQuotaRequestParent( + const RequestParams& aParams) override; + + virtual mozilla::ipc::IPCResult RecvPQuotaRequestConstructor( + PQuotaRequestParent* aActor, const RequestParams& aParams) override; + + virtual bool DeallocPQuotaRequestParent(PQuotaRequestParent* aActor) override; + + virtual mozilla::ipc::IPCResult RecvStartIdleMaintenance() override; + + virtual mozilla::ipc::IPCResult RecvStopIdleMaintenance() override; + + virtual mozilla::ipc::IPCResult RecvAbortOperationsForProcess( + const ContentParentId& aContentParentId) override; +}; + +class QuotaUsageRequestBase : public NormalOriginOperationBase, + public PQuotaUsageRequestParent { + public: + // May be overridden by subclasses if they need to perform work on the + // background thread before being run. + virtual void Init(Quota& aQuota); + + protected: + QuotaUsageRequestBase() + : NormalOriginOperationBase(Nullable<PersistenceType>(), + OriginScope::FromNull(), + /* aExclusive */ false) {} + + mozilla::Result<UsageInfo, nsresult> GetUsageForOrigin( + QuotaManager& aQuotaManager, PersistenceType aPersistenceType, + const GroupAndOrigin& aGroupAndOrigin); + + // Subclasses use this override to set the IPDL response value. + virtual void GetResponse(UsageRequestResponse& aResponse) = 0; + + private: + mozilla::Result<UsageInfo, nsresult> GetUsageForOriginEntries( + QuotaManager& aQuotaManager, PersistenceType aPersistenceType, + const GroupAndOrigin& aGroupAndOrigin, nsIFile& aDirectory, + bool aInitialized); + + void SendResults() override; + + // IPDL methods. + void ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult RecvCancel() final; +}; + +// A mix-in class to simplify operations that need to process every origin in +// one or more repositories. Sub-classes should call TraverseRepository in their +// DoDirectoryWork and implement a ProcessOrigin method for their per-origin +// logic. +class TraverseRepositoryHelper { + public: + TraverseRepositoryHelper() = default; + + protected: + virtual ~TraverseRepositoryHelper() = default; + + // If ProcessOrigin returns an error, TraverseRepository will immediately + // terminate and return the received error code to its caller. + nsresult TraverseRepository(QuotaManager& aQuotaManager, + PersistenceType aPersistenceType); + + private: + virtual const Atomic<bool>& GetIsCanceledFlag() = 0; + + virtual nsresult ProcessOrigin(QuotaManager& aQuotaManager, + nsIFile& aOriginDir, const bool aPersistent, + const PersistenceType aPersistenceType) = 0; +}; + +class GetUsageOp final : public QuotaUsageRequestBase, + public TraverseRepositoryHelper { + nsTArray<OriginUsage> mOriginUsages; + nsDataHashtable<nsCStringHashKey, uint32_t> mOriginUsagesIndex; + + bool mGetAll; + + public: + explicit GetUsageOp(const UsageRequestParams& aParams); + + private: + ~GetUsageOp() = default; + + void ProcessOriginInternal(QuotaManager* aQuotaManager, + const PersistenceType aPersistenceType, + const nsACString& aOrigin, + const int64_t aTimestamp, const bool aPersisted, + const uint64_t aUsage); + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + const Atomic<bool>& GetIsCanceledFlag() override; + + nsresult ProcessOrigin(QuotaManager& aQuotaManager, nsIFile& aOriginDir, + const bool aPersistent, + const PersistenceType aPersistenceType) override; + + void GetResponse(UsageRequestResponse& aResponse) override; +}; + +class GetOriginUsageOp final : public QuotaUsageRequestBase { + nsCString mSuffix; + nsCString mGroup; + uint64_t mUsage; + uint64_t mFileUsage; + bool mFromMemory; + + public: + explicit GetOriginUsageOp(const UsageRequestParams& aParams); + + private: + ~GetOriginUsageOp() = default; + + virtual nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void GetResponse(UsageRequestResponse& aResponse) override; +}; + +class QuotaRequestBase : public NormalOriginOperationBase, + public PQuotaRequestParent { + public: + // May be overridden by subclasses if they need to perform work on the + // background thread before being run. + virtual void Init(Quota& aQuota); + + protected: + explicit QuotaRequestBase(bool aExclusive) + : NormalOriginOperationBase(Nullable<PersistenceType>(), + OriginScope::FromNull(), aExclusive) {} + + // Subclasses use this override to set the IPDL response value. + virtual void GetResponse(RequestResponse& aResponse) = 0; + + private: + virtual void SendResults() override; + + // IPDL methods. + virtual void ActorDestroy(ActorDestroyReason aWhy) override; +}; + +class StorageNameOp final : public QuotaRequestBase { + nsString mName; + + public: + StorageNameOp(); + + void Init(Quota& aQuota) override; + + private: + ~StorageNameOp() = default; + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class InitializedRequestBase : public QuotaRequestBase { + protected: + bool mInitialized; + + public: + void Init(Quota& aQuota) override; + + protected: + InitializedRequestBase(); +}; + +class StorageInitializedOp final : public InitializedRequestBase { + private: + ~StorageInitializedOp() = default; + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class TemporaryStorageInitializedOp final : public InitializedRequestBase { + private: + ~TemporaryStorageInitializedOp() = default; + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class InitOp final : public QuotaRequestBase { + public: + InitOp(); + + void Init(Quota& aQuota) override; + + private: + ~InitOp() = default; + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class InitTemporaryStorageOp final : public QuotaRequestBase { + public: + InitTemporaryStorageOp(); + + void Init(Quota& aQuota) override; + + private: + ~InitTemporaryStorageOp() = default; + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class InitializeOriginRequestBase : public QuotaRequestBase { + protected: + nsCString mSuffix; + nsCString mGroup; + bool mCreated; + + public: + void Init(Quota& aQuota) override; + + protected: + InitializeOriginRequestBase(PersistenceType aPersistenceType, + const PrincipalInfo& aPrincipalInfo); +}; + +class InitializePersistentOriginOp final : public InitializeOriginRequestBase { + public: + explicit InitializePersistentOriginOp(const RequestParams& aParams); + + private: + ~InitializePersistentOriginOp() = default; + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class InitializeTemporaryOriginOp final : public InitializeOriginRequestBase { + public: + explicit InitializeTemporaryOriginOp(const RequestParams& aParams); + + private: + ~InitializeTemporaryOriginOp() = default; + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class ResetOrClearOp final : public QuotaRequestBase { + const bool mClear; + + public: + explicit ResetOrClearOp(bool aClear); + + void Init(Quota& aQuota) override; + + private: + ~ResetOrClearOp() = default; + + void DeleteFiles(QuotaManager& aQuotaManager); + + void DeleteStorageFile(QuotaManager& aQuotaManager); + + virtual nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + virtual void GetResponse(RequestResponse& aResponse) override; +}; + +class ClearRequestBase : public QuotaRequestBase { + protected: + explicit ClearRequestBase(bool aExclusive) : QuotaRequestBase(aExclusive) { + AssertIsOnOwningThread(); + } + + void DeleteFiles(QuotaManager& aQuotaManager, + PersistenceType aPersistenceType); + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; +}; + +class ClearOriginOp final : public ClearRequestBase { + const ClearResetOriginParams mParams; + const bool mMatchAll; + + public: + explicit ClearOriginOp(const RequestParams& aParams); + + void Init(Quota& aQuota) override; + + private: + ~ClearOriginOp() = default; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class ClearDataOp final : public ClearRequestBase { + const ClearDataParams mParams; + + public: + explicit ClearDataOp(const RequestParams& aParams); + + void Init(Quota& aQuota) override; + + private: + ~ClearDataOp() = default; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class ResetOriginOp final : public QuotaRequestBase { + public: + explicit ResetOriginOp(const RequestParams& aParams); + + void Init(Quota& aQuota) override; + + private: + ~ResetOriginOp() = default; + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class PersistRequestBase : public QuotaRequestBase { + const PrincipalInfo mPrincipalInfo; + + protected: + nsCString mSuffix; + nsCString mGroup; + + public: + void Init(Quota& aQuota) override; + + protected: + explicit PersistRequestBase(const PrincipalInfo& aPrincipalInfo); +}; + +class PersistedOp final : public PersistRequestBase { + bool mPersisted; + + public: + explicit PersistedOp(const RequestParams& aParams); + + private: + ~PersistedOp() = default; + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class PersistOp final : public PersistRequestBase { + public: + explicit PersistOp(const RequestParams& aParams); + + private: + ~PersistOp() = default; + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class EstimateOp final : public QuotaRequestBase { + nsCString mGroup; + uint64_t mUsage; + uint64_t mLimit; + + public: + explicit EstimateOp(const RequestParams& aParams); + + private: + ~EstimateOp() = default; + + virtual nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class ListOriginsOp final : public QuotaRequestBase, + public TraverseRepositoryHelper { + // XXX Bug 1521541 will make each origin has it's own state. + nsTArray<nsCString> mOrigins; + + public: + ListOriginsOp(); + + void Init(Quota& aQuota) override; + + private: + ~ListOriginsOp() = default; + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + const Atomic<bool>& GetIsCanceledFlag() override; + + nsresult ProcessOrigin(QuotaManager& aQuotaManager, nsIFile& aOriginDir, + const bool aPersistent, + const PersistenceType aPersistenceType) override; + + void GetResponse(RequestResponse& aResponse) override; +}; + +/******************************************************************************* + * Other class declarations + ******************************************************************************/ + +class StoragePressureRunnable final : public Runnable { + const uint64_t mUsage; + + public: + explicit StoragePressureRunnable(uint64_t aUsage) + : Runnable("dom::quota::QuotaObject::StoragePressureRunnable"), + mUsage(aUsage) {} + + private: + ~StoragePressureRunnable() = default; + + NS_DECL_NSIRUNNABLE +}; + +class RecordQuotaInfoLoadTimeHelper final : public Runnable { + // TimeStamps that are set on the IO thread. + LazyInitializedOnceNotNull<const TimeStamp> mStartTime; + LazyInitializedOnceNotNull<const TimeStamp> mEndTime; + + // A TimeStamp that is set on the main thread. + LazyInitializedOnceNotNull<const TimeStamp> mInitializedTime; + + public: + RecordQuotaInfoLoadTimeHelper() + : Runnable("dom::quota::RecordQuotaInfoLoadTimeHelper") {} + + void Start(); + + void End(); + + private: + ~RecordQuotaInfoLoadTimeHelper() = default; + + NS_DECL_NSIRUNNABLE +}; + +/******************************************************************************* + * Helper classes + ******************************************************************************/ + +#ifdef QM_PRINCIPALINFO_VERIFICATION_ENABLED + +class PrincipalVerifier final : public Runnable { + nsTArray<PrincipalInfo> mPrincipalInfos; + + public: + static already_AddRefed<PrincipalVerifier> CreateAndDispatch( + nsTArray<PrincipalInfo>&& aPrincipalInfos); + + private: + explicit PrincipalVerifier(nsTArray<PrincipalInfo>&& aPrincipalInfos) + : Runnable("dom::quota::PrincipalVerifier"), + mPrincipalInfos(std::move(aPrincipalInfos)) { + AssertIsOnIOThread(); + } + + virtual ~PrincipalVerifier() = default; + + Result<Ok, nsCString> CheckPrincipalInfoValidity( + const PrincipalInfo& aPrincipalInfo); + + NS_DECL_NSIRUNNABLE +}; + +#endif + +/******************************************************************************* + * Helper Functions + ******************************************************************************/ + +template <typename T, bool = std::is_unsigned_v<T>> +struct IntChecker { + static void Assert(T aInt) { + static_assert(std::is_integral_v<T>, "Not an integer!"); + MOZ_ASSERT(aInt >= 0); + } +}; + +template <typename T> +struct IntChecker<T, true> { + static void Assert(T aInt) { + static_assert(std::is_integral_v<T>, "Not an integer!"); + } +}; + +template <typename T> +void AssertNoOverflow(uint64_t aDest, T aArg) { + IntChecker<T>::Assert(aDest); + IntChecker<T>::Assert(aArg); + MOZ_ASSERT(UINT64_MAX - aDest >= uint64_t(aArg)); +} + +template <typename T, typename U> +void AssertNoUnderflow(T aDest, U aArg) { + IntChecker<T>::Assert(aDest); + IntChecker<T>::Assert(aArg); + MOZ_ASSERT(uint64_t(aDest) >= uint64_t(aArg)); +} + +inline bool IsDotFile(const nsAString& aFileName) { + return QuotaManager::IsDotFile(aFileName); +} + +inline bool IsOSMetadata(const nsAString& aFileName) { + return QuotaManager::IsOSMetadata(aFileName); +} + +bool IsOriginMetadata(const nsAString& aFileName) { + return aFileName.EqualsLiteral(METADATA_FILE_NAME) || + aFileName.EqualsLiteral(METADATA_V2_FILE_NAME) || + IsOSMetadata(aFileName); +} + +bool IsTempMetadata(const nsAString& aFileName) { + return aFileName.EqualsLiteral(METADATA_TMP_FILE_NAME) || + aFileName.EqualsLiteral(METADATA_V2_TMP_FILE_NAME); +} + +// Return whether the group was actually updated. +Result<bool, nsresult> MaybeUpdateGroupForOrigin( + GroupAndOrigin& aGroupAndOrigin) { + MOZ_ASSERT(!NS_IsMainThread()); + + bool updated = false; + + if (aGroupAndOrigin.mOrigin.EqualsLiteral(kChromeOrigin)) { + if (!aGroupAndOrigin.mGroup.EqualsLiteral(kChromeOrigin)) { + aGroupAndOrigin.mGroup.AssignLiteral(kChromeOrigin); + updated = true; + } + } else { + OriginAttributes originAttributes; + nsCString originNoSuffix; + QM_TRY(OkIf(originAttributes.PopulateFromOrigin(aGroupAndOrigin.mOrigin, + originNoSuffix)), + Err(NS_ERROR_FAILURE)); + + nsCString suffix; + originAttributes.CreateSuffix(suffix); + + RefPtr<MozURL> url; + QM_TRY(MozURL::Init(getter_AddRefs(url), originNoSuffix), QM_PROPAGATE, + [&originNoSuffix](const nsresult) { + QM_WARNING("A URL %s is not recognized by MozURL", + originNoSuffix.get()); + }); + + QM_TRY_INSPECT(const auto& baseDomain, + MOZ_TO_RESULT_INVOKE_TYPED(nsAutoCString, *url, BaseDomain)); + + const nsCString upToDateGroup = baseDomain + suffix; + + if (aGroupAndOrigin.mGroup != upToDateGroup) { + aGroupAndOrigin.mGroup = upToDateGroup; + updated = true; + +#ifdef QM_PRINCIPALINFO_VERIFICATION_ENABLED + ContentPrincipalInfo contentPrincipalInfo; + contentPrincipalInfo.attrs() = originAttributes; + contentPrincipalInfo.originNoSuffix() = originNoSuffix; + contentPrincipalInfo.spec() = originNoSuffix; + contentPrincipalInfo.baseDomain() = baseDomain; + + PrincipalInfo principalInfo(contentPrincipalInfo); + + nsTArray<PrincipalInfo> principalInfos; + principalInfos.AppendElement(principalInfo); + + RefPtr<PrincipalVerifier> principalVerifier = + PrincipalVerifier::CreateAndDispatch(std::move(principalInfos)); +#endif + } + } + + return updated; +} + +} // namespace + +BackgroundThreadObject::BackgroundThreadObject() + : mOwningThread(GetCurrentEventTarget()) { + AssertIsOnOwningThread(); +} + +BackgroundThreadObject::BackgroundThreadObject(nsIEventTarget* 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 + +nsIEventTarget* BackgroundThreadObject::OwningThread() const { + MOZ_ASSERT(mOwningThread); + return mOwningThread; +} + +bool IsOnIOThread() { + QuotaManager* quotaManager = QuotaManager::Get(); + NS_ASSERTION(quotaManager, "Must have a manager here!"); + + bool currentThread; + return NS_SUCCEEDED( + quotaManager->IOThread()->IsOnCurrentThread(¤tThread)) && + currentThread; +} + +void AssertIsOnIOThread() { + NS_ASSERTION(IsOnIOThread(), "Running on the wrong thread!"); +} + +void AssertCurrentThreadOwnsQuotaMutex() { +#ifdef DEBUG + QuotaManager* quotaManager = QuotaManager::Get(); + NS_ASSERTION(quotaManager, "Must have a manager here!"); + + quotaManager->AssertCurrentThreadOwnsQuotaMutex(); +#endif +} + +void ReportInternalError(const char* aFile, uint32_t aLine, const char* aStr) { + // Get leaf of file path + for (const char* p = aFile; *p; ++p) { + if (*p == '/' && *(p + 1)) { + aFile = p + 1; + } + } + + nsContentUtils::LogSimpleConsoleError( + NS_ConvertUTF8toUTF16( + nsPrintfCString("Quota %s: %s:%" PRIu32, aStr, aFile, aLine)), + "quota", 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; +bool gCreateFailed = false; +mozilla::Atomic<bool> gShutdown(false); + +// A time stamp that can only be accessed on the main thread. +TimeStamp gLastOSWake; + +typedef nsTArray<CheckedUnsafePtr<NormalOriginOperationBase>> + NormalOriginOpArray; +StaticAutoPtr<NormalOriginOpArray> gNormalOriginOps; + +// Constants for temporary storage limit computing. +static const uint32_t kDefaultChunkSizeKB = 10 * 1024; + +void RegisterNormalOriginOp(NormalOriginOperationBase& aNormalOriginOp) { + AssertIsOnBackgroundThread(); + + if (!gNormalOriginOps) { + gNormalOriginOps = new NormalOriginOpArray(); + } + + gNormalOriginOps->AppendElement(&aNormalOriginOp); +} + +void UnregisterNormalOriginOp(NormalOriginOperationBase& aNormalOriginOp) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(gNormalOriginOps); + + gNormalOriginOps->RemoveElement(&aNormalOriginOp); + + if (gNormalOriginOps->IsEmpty()) { + gNormalOriginOps = nullptr; + } +} + +class StorageOperationBase { + protected: + struct OriginProps { + enum Type { eChrome, eContent, eObsolete, eInvalid }; + + nsCOMPtr<nsIFile> mDirectory; + nsString mLeafName; + nsCString mSpec; + OriginAttributes mAttrs; + int64_t mTimestamp; + QuotaInfo mQuotaInfo; + nsCString mOriginalSuffix; + + Type mType; + bool mNeedsRestore; + bool mNeedsRestore2; + bool mIgnore; + + public: + explicit OriginProps() + : mTimestamp(0), + mType(eContent), + mNeedsRestore(false), + mNeedsRestore2(false), + mIgnore(false) {} + + nsresult Init(nsIFile* aDirectory); + }; + + nsTArray<OriginProps> mOriginProps; + + nsCOMPtr<nsIFile> mDirectory; + + const bool mPersistent; + + public: + StorageOperationBase(nsIFile* aDirectory, bool aPersistent) + : mDirectory(aDirectory), mPersistent(aPersistent) { + 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); + + nsresult RemoveObsoleteOrigin(const OriginProps& aOriginProps); + + nsresult ProcessOriginDirectories(); + + virtual nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) = 0; +}; + +class MOZ_STACK_CLASS OriginParser final { + public: + enum ResultType { InvalidOrigin, ObsoleteOrigin, ValidOrigin }; + + private: + using Tokenizer = + nsCCharSeparatedTokenizerTemplate<NS_TokenizerIgnoreNothing>; + + enum SchemeType { eNone, eFile, eAbout, eChrome }; + + enum State { + eExpectingAppIdOrScheme, + eExpectingInMozBrowser, + eExpectingScheme, + eExpectingEmptyToken1, + eExpectingEmptyToken2, + eExpectingEmptyTokenOrUniversalFileOrigin, + eExpectingHost, + eExpectingPort, + eExpectingEmptyTokenOrDriveLetterOrPathnameComponent, + eExpectingEmptyTokenOrPathnameComponent, + eExpectingEmptyToken1OrHost, + + // We transit from eExpectingHost to this state when we encounter a host + // beginning with "[" which indicates an IPv6 literal. Because we mangle the + // IPv6 ":" delimiter to be a "+", we will receive separate tokens for each + // portion of the IPv6 address, including a final token that ends with "]". + // (Note that we do not mangle "[" or "]".) Note that the URL spec + // explicitly disclaims support for "<zone_id>" and so we don't have to deal + // with that. + eExpectingIPV6Token, + eComplete, + eHandledTrailingSeparator + }; + + const nsCString mOrigin; + Tokenizer mTokenizer; + + nsCString mScheme; + nsCString mHost; + Nullable<uint32_t> mPort; + nsTArray<nsCString> mPathnameComponents; + nsCString mHandledTokens; + + SchemeType mSchemeType; + State mState; + bool mInIsolatedMozBrowser; + bool mUniversalFileOrigin; + bool mMaybeDriveLetter; + bool mError; + bool mMaybeObsolete; + + // Number of group which a IPv6 address has. Should be less than 9. + uint8_t mIPGroup; + + public: + explicit OriginParser(const nsACString& aOrigin) + : mOrigin(aOrigin), + mTokenizer(aOrigin, '+'), + mPort(), + mSchemeType(eNone), + mState(eExpectingAppIdOrScheme), + mInIsolatedMozBrowser(false), + mUniversalFileOrigin(false), + mMaybeDriveLetter(false), + mError(false), + mMaybeObsolete(false), + mIPGroup(0) {} + + static ResultType ParseOrigin(const nsACString& aOrigin, nsCString& aSpec, + OriginAttributes* aAttrs, + nsCString& aOriginalSuffix); + + ResultType Parse(nsACString& aSpec); + + private: + void HandleScheme(const nsDependentCSubstring& aToken); + + void HandlePathnameComponent(const nsDependentCSubstring& aToken); + + void HandleToken(const nsDependentCSubstring& aToken); + + void HandleTrailingSeparator(); +}; + +class RepositoryOperationBase : public StorageOperationBase { + public: + RepositoryOperationBase(nsIFile* aDirectory, bool aPersistent) + : StorageOperationBase(aDirectory, aPersistent) {} + + nsresult ProcessRepository(); + + protected: + virtual ~RepositoryOperationBase() = default; + + template <typename UpgradeMethod> + nsresult MaybeUpgradeClients(const OriginProps& aOriginsProps, + UpgradeMethod aMethod); + + private: + 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; + + public: + CreateOrUpgradeDirectoryMetadataHelper(nsIFile* aDirectory, bool aPersistent) + : RepositoryOperationBase(aDirectory, aPersistent) {} + + private: + nsresult MaybeUpgradeOriginDirectory(nsIFile* aDirectory); + + nsresult PrepareOriginDirectory(OriginProps& aOriginProps, + bool* aRemoved) override; + + nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) override; +}; + +class UpgradeStorageFrom0_0To1_0Helper final : public RepositoryOperationBase { + public: + UpgradeStorageFrom0_0To1_0Helper(nsIFile* aDirectory, bool aPersistent) + : RepositoryOperationBase(aDirectory, aPersistent) {} + + private: + nsresult PrepareOriginDirectory(OriginProps& aOriginProps, + bool* aRemoved) override; + + nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) override; +}; + +class UpgradeStorageFrom1_0To2_0Helper final : public RepositoryOperationBase { + public: + UpgradeStorageFrom1_0To2_0Helper(nsIFile* aDirectory, bool aPersistent) + : RepositoryOperationBase(aDirectory, aPersistent) {} + + 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); + + /** + * Strip obsolete origin attributes from the origin in aOriginProps. + * + * @param aOriginProps the properties of the origin to check. + * + * @return whether obsolete origin attributes were stripped. + */ + Result<bool, nsresult> MaybeStripObsoleteOriginAttributes( + const OriginProps& aOriginProps); + + nsresult PrepareOriginDirectory(OriginProps& aOriginProps, + bool* aRemoved) override; + + nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) override; +}; + +class UpgradeStorageFrom2_0To2_1Helper final : public RepositoryOperationBase { + public: + UpgradeStorageFrom2_0To2_1Helper(nsIFile* aDirectory, bool aPersistent) + : RepositoryOperationBase(aDirectory, aPersistent) {} + + private: + nsresult PrepareOriginDirectory(OriginProps& aOriginProps, + bool* aRemoved) override; + + nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) override; +}; + +class UpgradeStorageFrom2_1To2_2Helper final : public RepositoryOperationBase { + public: + UpgradeStorageFrom2_1To2_2Helper(nsIFile* aDirectory, bool aPersistent) + : RepositoryOperationBase(aDirectory, aPersistent) {} + + 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 { + public: + RestoreDirectoryMetadata2Helper(nsIFile* aDirectory, bool aPersistent) + : StorageOperationBase(aDirectory, aPersistent) {} + + nsresult RestoreMetadata2File(); + + private: + nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) override; +}; + +auto MakeSanitizedOriginCString(const nsACString& aOrigin) { +#ifdef XP_WIN + NS_ASSERTION(!strcmp(QuotaManager::kReplaceChars, + FILE_ILLEGAL_CHARACTERS FILE_PATH_SEPARATOR), + "Illegal file characters have changed!"); +#endif + + nsAutoCString res{aOrigin}; + + res.ReplaceChar(QuotaManager::kReplaceChars, '+'); + + return res; +} + +auto MakeSanitizedOriginString(const nsACString& aOrigin) { + // An origin string is ASCII-only, since it is obtained via + // nsIPrincipal::GetOrigin, which returns an ACString. + return NS_ConvertASCIItoUTF16(MakeSanitizedOriginCString(aOrigin)); +} + +Result<nsAutoString, nsresult> GetPathForStorage( + nsIFile& aBaseDir, const nsAString& aStorageName) { + QM_TRY_INSPECT(const auto& storageDir, + CloneFileAndAppend(aBaseDir, aStorageName)); + + QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_TYPED(nsAutoString, storageDir, GetPath)); +} + +int64_t GetLastModifiedTime(nsIFile* aFile, bool aPersistent) { + AssertIsOnIOThread(); + MOZ_ASSERT(aFile); + + 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 bool& isDirectory, + MOZ_TO_RESULT_INVOKE(aFile, IsDirectory)); + + if (!isDirectory) { + QM_TRY_INSPECT( + const auto& leafName, + MOZ_TO_RESULT_INVOKE_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(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; + } + return NS_OK; + } + + QM_TRY(CollectEachFile(*aFile, + [&aTimestamp](const nsCOMPtr<nsIFile>& file) + -> Result<mozilla::Ok, nsresult> { + QM_TRY(GetLastModifiedTime(file, aTimestamp)); + + return Ok{}; + })); + + return NS_OK; + } + }; + + if (aPersistent) { + return PR_Now(); + } + + int64_t timestamp = INT64_MIN; + nsresult rv = Helper::GetLastModifiedTime(aFile, ×tamp); + if (NS_FAILED(rv)) { + timestamp = PR_Now(); + } + + return timestamp; +} + +// Returns a bool indicating whether the directory was newly created. +Result<bool, nsresult> EnsureDirectory(nsIFile& aDirectory) { + AssertIsOnIOThread(); + + // TODO: Convert to mapOrElse once mozilla::Result supports it. + QM_TRY_INSPECT( + const auto& exists, + MOZ_TO_RESULT_INVOKE(aDirectory, Create, nsIFile::DIRECTORY_TYPE, 0755) + .andThen(OkToOk<false>) + .orElse(ErrToOkOrErr<NS_ERROR_FILE_ALREADY_EXISTS, true>)); + + if (exists) { + QM_TRY_INSPECT(const bool& isDirectory, + MOZ_TO_RESULT_INVOKE(aDirectory, IsDirectory)); + QM_TRY(OkIf(isDirectory), Err(NS_ERROR_UNEXPECTED)); + } + + return !exists; +} + +enum FileFlag { kTruncateFileFlag, kUpdateFileFlag, kAppendFileFlag }; + +Result<nsCOMPtr<nsIOutputStream>, nsresult> GetOutputStream( + nsIFile& aFile, FileFlag aFileFlag) { + AssertIsOnIOThread(); + + switch (aFileFlag) { + case kTruncateFileFlag: + QM_TRY_RETURN(NS_NewLocalFileOutputStream(&aFile)); + + case kUpdateFileFlag: { + QM_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE(&aFile, Exists)); + + if (!exists) { + return nsCOMPtr<nsIOutputStream>(); + } + + QM_TRY_INSPECT(const auto& stream, NS_NewLocalFileStream(&aFile)); + + nsCOMPtr<nsIOutputStream> outputStream = do_QueryInterface(stream); + QM_TRY(OkIf(outputStream), Err(NS_ERROR_FAILURE)); + + return outputStream; + } + + case kAppendFileFlag: + QM_TRY_RETURN(NS_NewLocalFileOutputStream( + &aFile, PR_WRONLY | PR_CREATE_FILE | PR_APPEND)); + + default: + MOZ_CRASH("Should never get here!"); + } +} + +Result<nsCOMPtr<nsIBinaryOutputStream>, nsresult> GetBinaryOutputStream( + nsIFile& aFile, FileFlag aFileFlag) { + QM_TRY_UNWRAP(auto outputStream, GetOutputStream(aFile, aFileFlag)); + + QM_TRY(OkIf(outputStream), Err(NS_ERROR_UNEXPECTED)); + + return nsCOMPtr<nsIBinaryOutputStream>( + NS_NewObjectOutputStream(outputStream)); +} + +void GetJarPrefix(bool aInIsolatedMozBrowser, nsACString& aJarPrefix) { + aJarPrefix.Truncate(); + + // Fallback. + if (!aInIsolatedMozBrowser) { + return; + } + + // AppId is an unused b2g identifier. Let's set it to 0 all the time (see bug + // 1320404). + // aJarPrefix = appId + "+" + { 't', 'f' } + "+"; + aJarPrefix.AppendInt(0); // TODO: this is the appId, to be removed. + aJarPrefix.Append('+'); + aJarPrefix.Append(aInIsolatedMozBrowser ? 't' : 'f'); + aJarPrefix.Append('+'); +} + +nsresult CreateDirectoryMetadata(nsIFile& aDirectory, int64_t aTimestamp, + const QuotaInfo& aQuotaInfo) { + AssertIsOnIOThread(); + + OriginAttributes groupAttributes; + + nsCString groupNoSuffix; + QM_TRY(OkIf(groupAttributes.PopulateFromOrigin(aQuotaInfo.mGroup, + groupNoSuffix)), + NS_ERROR_FAILURE); + + nsCString groupPrefix; + GetJarPrefix(groupAttributes.mInIsolatedMozBrowser, groupPrefix); + + nsCString group = groupPrefix + groupNoSuffix; + + OriginAttributes originAttributes; + + nsCString originNoSuffix; + QM_TRY(OkIf(originAttributes.PopulateFromOrigin(aQuotaInfo.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_TYPED( + nsCOMPtr<nsIFile>, aDirectory, Clone)); + + QM_TRY(file->Append(nsLiteralString(METADATA_TMP_FILE_NAME))); + + QM_TRY_INSPECT(const auto& stream, + GetBinaryOutputStream(*file, kTruncateFileFlag)); + MOZ_ASSERT(stream); + + QM_TRY(stream->Write64(aTimestamp)); + + QM_TRY(stream->WriteStringZ(group.get())); + + QM_TRY(stream->WriteStringZ(origin.get())); + + // Currently unused (used to be isApp). + QM_TRY(stream->WriteBoolean(false)); + + QM_TRY(stream->Flush()); + + QM_TRY(stream->Close()); + + QM_TRY(file->RenameTo(nullptr, nsLiteralString(METADATA_FILE_NAME))); + + return NS_OK; +} + +nsresult CreateDirectoryMetadata2(nsIFile& aDirectory, int64_t aTimestamp, + bool aPersisted, + const QuotaInfo& aQuotaInfo) { + AssertIsOnIOThread(); + + QM_TRY_INSPECT(const auto& file, MOZ_TO_RESULT_INVOKE_TYPED( + nsCOMPtr<nsIFile>, aDirectory, Clone)); + + QM_TRY(file->Append(nsLiteralString(METADATA_V2_TMP_FILE_NAME))); + + QM_TRY_INSPECT(const auto& stream, + GetBinaryOutputStream(*file, kTruncateFileFlag)); + MOZ_ASSERT(stream); + + QM_TRY(stream->Write64(aTimestamp)); + + QM_TRY(stream->WriteBoolean(aPersisted)); + + // Reserved data 1 + QM_TRY(stream->Write32(0)); + + // Reserved data 2 + QM_TRY(stream->Write32(0)); + + // The suffix isn't used right now, but we might need it in future. It's + // a bit of redundancy we can live with given how painful is to upgrade + // metadata files. + QM_TRY(stream->WriteStringZ(aQuotaInfo.mSuffix.get())); + + QM_TRY(stream->WriteStringZ(aQuotaInfo.mGroup.get())); + + QM_TRY(stream->WriteStringZ(aQuotaInfo.mOrigin.get())); + + // Currently unused (used to be isApp). + QM_TRY(stream->WriteBoolean(false)); + + QM_TRY(stream->Flush()); + + QM_TRY(stream->Close()); + + QM_TRY(file->RenameTo(nullptr, nsLiteralString(METADATA_V2_FILE_NAME))); + + return NS_OK; +} + +Result<nsCOMPtr<nsIBinaryInputStream>, nsresult> GetBinaryInputStream( + nsIFile& aDirectory, const nsAString& aFilename) { + MOZ_ASSERT(!NS_IsMainThread()); + + QM_TRY_INSPECT(const auto& file, MOZ_TO_RESULT_INVOKE_TYPED( + nsCOMPtr<nsIFile>, aDirectory, Clone)); + + QM_TRY(file->Append(aFilename)); + + QM_TRY_UNWRAP(auto stream, NS_NewLocalFileInputStream(file)); + + QM_TRY_INSPECT(const auto& bufferedStream, + NS_NewBufferedInputStream(stream.forget(), 512)); + + QM_TRY(OkIf(bufferedStream), Err(NS_ERROR_FAILURE)); + + return nsCOMPtr<nsIBinaryInputStream>( + NS_NewObjectInputStream(bufferedStream)); +} + +// This method computes and returns our best guess for the temporary storage +// limit (in bytes), based on available space. +uint64_t GetTemporaryStorageLimit(uint64_t aAvailableSpaceBytes) { + // 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; + } + + uint64_t availableSpaceKB = aAvailableSpaceBytes / 1024; + + // Prevent division by zero below. + uint32_t chunkSizeKB; + if (StaticPrefs::dom_quotaManager_temporaryStorage_chunkSize()) { + chunkSizeKB = StaticPrefs::dom_quotaManager_temporaryStorage_chunkSize(); + } else { + chunkSizeKB = kDefaultChunkSizeKB; + } + + // Grow/shrink in chunkSizeKB units, deliberately, so that in the common case + // we don't shrink temporary storage and evict origin data every time we + // initialize. + availableSpaceKB = (availableSpaceKB / chunkSizeKB) * chunkSizeKB; + + // Allow temporary storage to consume up to half the available space. + return availableSpaceKB * .50 * 1024; +} + +} // namespace + +/******************************************************************************* + * Exported functions + ******************************************************************************/ + +void InitializeQuotaManager() { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!gQuotaManagerInitialized); + + if (!QuotaManager::IsRunningGTests()) { + // This service has to be started on the main thread currently. + nsCOMPtr<mozIStorageService> ss; + if (NS_WARN_IF(!(ss = do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID)))) { + NS_WARNING("Failed to get storage service!"); + } + } + + if (NS_FAILED(QuotaManager::Initialize())) { + NS_WARNING("Failed to initialize quota manager!"); + } + +#ifdef DEBUG + gQuotaManagerInitialized = true; +#endif +} + +PQuotaParent* AllocPQuotaParent() { + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(QuotaManager::IsShuttingDown())) { + return nullptr; + } + + auto actor = MakeRefPtr<Quota>(); + + return actor.forget().take(); +} + +bool DeallocPQuotaParent(PQuotaParent* aActor) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + RefPtr<Quota> actor = dont_AddRef(static_cast<Quota*>(aActor)); + return true; +} + +bool RecvShutdownQuotaManager() { + AssertIsOnBackgroundThread(); + + QuotaManager::ShutdownInstance(); + + return true; +} + +/******************************************************************************* + * Directory lock + ******************************************************************************/ + +int64_t DirectoryLock::Id() const { return GetDirectoryLockImpl(this)->Id(); } + +PersistenceType DirectoryLock::GetPersistenceType() const { + return GetDirectoryLockImpl(this)->GetPersistenceType(); +} + +quota::GroupAndOrigin DirectoryLock::GroupAndOrigin() const { + return GetDirectoryLockImpl(this)->GroupAndOrigin(); +} + +const nsACString& DirectoryLock::Origin() const { + return GetDirectoryLockImpl(this)->Origin(); +} + +Client::Type DirectoryLock::ClientType() const { + return GetDirectoryLockImpl(this)->ClientType(); +} + +already_AddRefed<DirectoryLock> DirectoryLock::Specialize( + PersistenceType aPersistenceType, + const quota::GroupAndOrigin& aGroupAndOrigin, + Client::Type aClientType) const { + return GetDirectoryLockImpl(this)->Specialize(aPersistenceType, + aGroupAndOrigin, aClientType); +} + +void DirectoryLock::Log() const { GetDirectoryLockImpl(this)->Log(); } + +DirectoryLockImpl::DirectoryLockImpl( + MovingNotNull<RefPtr<QuotaManager>> aQuotaManager, const int64_t aId, + const Nullable<PersistenceType>& aPersistenceType, const nsACString& aGroup, + const OriginScope& aOriginScope, const Nullable<Client::Type>& aClientType, + const bool aExclusive, const bool aInternal, + const ShouldUpdateLockIdTableFlag aShouldUpdateLockIdTableFlag, + RefPtr<OpenDirectoryListener> aOpenListener) + : mQuotaManager(std::move(aQuotaManager)), + mPersistenceType(aPersistenceType), + mGroup(aGroup), + mOriginScope(aOriginScope), + mClientType(aClientType), + mId(aId), + mExclusive(aExclusive), + mInternal(aInternal), + mShouldUpdateLockIdTable(aShouldUpdateLockIdTableFlag == + ShouldUpdateLockIdTableFlag::Yes), + mRegistered(false) { + AssertIsOnOwningThread(); + MOZ_ASSERT_IF(aOriginScope.IsOrigin(), !aOriginScope.GetOrigin().IsEmpty()); + MOZ_ASSERT_IF(!aInternal, !aPersistenceType.IsNull()); + MOZ_ASSERT_IF(!aInternal, + aPersistenceType.Value() != PERSISTENCE_TYPE_INVALID); + MOZ_ASSERT_IF(!aInternal, !aGroup.IsEmpty()); + MOZ_ASSERT_IF(!aInternal, aOriginScope.IsOrigin()); + MOZ_ASSERT_IF(!aInternal, !aClientType.IsNull()); + MOZ_ASSERT_IF(!aInternal, aClientType.Value() < Client::TypeMax()); + MOZ_ASSERT_IF(!aInternal, aOpenListener); + + if (aOpenListener) { + mOpenListener.init(WrapNotNullUnchecked(std::move(aOpenListener))); + } +} + +DirectoryLockImpl::~DirectoryLockImpl() { + AssertIsOnOwningThread(); + + for (NotNull<RefPtr<DirectoryLockImpl>> blockingLock : mBlocking) { + blockingLock->MaybeUnblock(*this); + } + + mBlocking.Clear(); + + if (mRegistered) { + mQuotaManager->UnregisterDirectoryLock(*this); + } + + MOZ_ASSERT(!mRegistered); +} + +#ifdef DEBUG + +void DirectoryLockImpl::AssertIsOnOwningThread() const { + mQuotaManager->AssertIsOnOwningThread(); +} + +#endif // DEBUG + +bool DirectoryLockImpl::Overlaps(const DirectoryLockImpl& aLock) const { + AssertIsOnOwningThread(); + + // If the persistence types don't overlap, the op can proceed. + if (!aLock.mPersistenceType.IsNull() && !mPersistenceType.IsNull() && + aLock.mPersistenceType.Value() != mPersistenceType.Value()) { + return false; + } + + // If the origin scopes don't overlap, the op can proceed. + bool match = aLock.mOriginScope.Matches(mOriginScope); + if (!match) { + return false; + } + + // If the client types don't overlap, the op can proceed. + if (!aLock.mClientType.IsNull() && !mClientType.IsNull() && + aLock.mClientType.Value() != mClientType.Value()) { + return false; + } + + // Otherwise, when all attributes overlap (persistence type, origin scope and + // client type) the op must wait. + return true; +} + +bool DirectoryLockImpl::MustWaitFor(const DirectoryLockImpl& aLock) const { + AssertIsOnOwningThread(); + + // Waiting is never required if the ops in comparison represent shared locks. + if (!aLock.mExclusive && !mExclusive) { + return false; + } + + // Wait if the ops overlap. + return Overlaps(aLock); +} + +void DirectoryLockImpl::NotifyOpenListener() { + AssertIsOnOwningThread(); + + if (mInvalidated) { + (*mOpenListener)->DirectoryLockFailed(); + } else { + (*mOpenListener)->DirectoryLockAcquired(this); + } + + mOpenListener.destroy(); + + mQuotaManager->RemovePendingDirectoryLock(*this); + + mPending.Flip(); +} + +already_AddRefed<DirectoryLock> DirectoryLockImpl::Specialize( + PersistenceType aPersistenceType, + const quota::GroupAndOrigin& aGroupAndOrigin, + Client::Type aClientType) const { + AssertIsOnOwningThread(); + MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_INVALID); + MOZ_ASSERT(!aGroupAndOrigin.mGroup.IsEmpty()); + MOZ_ASSERT(!aGroupAndOrigin.mOrigin.IsEmpty()); + MOZ_ASSERT(aClientType < Client::TypeMax()); + MOZ_ASSERT(!mOpenListener); + MOZ_ASSERT(mBlockedOn.IsEmpty()); + + if (NS_WARN_IF(mExclusive)) { + return nullptr; + } + + RefPtr<DirectoryLockImpl> lock = new DirectoryLockImpl( + mQuotaManager, mQuotaManager->GenerateDirectoryLockId(), + Nullable<PersistenceType>(aPersistenceType), aGroupAndOrigin.mGroup, + OriginScope::FromOrigin(aGroupAndOrigin.mOrigin), + Nullable<Client::Type>(aClientType), + /* aExclusive */ false, mInternal, ShouldUpdateLockIdTableFlag::Yes, + /* aOpenListener */ nullptr); + if (NS_WARN_IF(!Overlaps(*lock))) { + return nullptr; + } + +#ifdef DEBUG + for (uint32_t index = mQuotaManager->mDirectoryLocks.Length(); index > 0; + index--) { + DirectoryLockImpl* existingLock = mQuotaManager->mDirectoryLocks[index - 1]; + if (existingLock != this && !existingLock->MustWaitFor(*this)) { + MOZ_ASSERT(!existingLock->MustWaitFor(*lock)); + } + } +#endif + + for (const auto& blockedLock : mBlocking) { + if (blockedLock->MustWaitFor(*lock)) { + lock->AddBlockingLock(*blockedLock); + blockedLock->AddBlockedOnLock(*lock); + } + } + + mQuotaManager->RegisterDirectoryLock(*lock); + + if (mInvalidated) { + lock->Invalidate(); + } + + return lock.forget(); +} + +void DirectoryLockImpl::Log() const { + AssertIsOnOwningThread(); + + if (!QM_LOG_TEST()) { + return; + } + + QM_LOG(("DirectoryLockImpl [%p]", this)); + + nsCString persistenceType; + if (mPersistenceType.IsNull()) { + persistenceType.AssignLiteral("null"); + } else { + persistenceType.Assign(PersistenceTypeToString(mPersistenceType.Value())); + } + QM_LOG((" mPersistenceType: %s", persistenceType.get())); + + QM_LOG((" mGroup: %s", mGroup.get())); + + nsCString originScope; + if (mOriginScope.IsOrigin()) { + originScope.AssignLiteral("origin:"); + originScope.Append(mOriginScope.GetOrigin()); + } else if (mOriginScope.IsPrefix()) { + originScope.AssignLiteral("prefix:"); + originScope.Append(mOriginScope.GetOriginNoSuffix()); + } else if (mOriginScope.IsPattern()) { + originScope.AssignLiteral("pattern:"); + // Can't call GetJSONPattern since it only works on the main thread. + } else { + MOZ_ASSERT(mOriginScope.IsNull()); + originScope.AssignLiteral("null"); + } + QM_LOG((" mOriginScope: %s", originScope.get())); + + const auto clientType = mClientType.IsNull() + ? nsAutoCString{"null"_ns} + : Client::TypeToText(mClientType.Value()); + QM_LOG((" mClientType: %s", clientType.get())); + + nsCString blockedOnString; + for (auto blockedOn : mBlockedOn) { + blockedOnString.Append( + nsPrintfCString(" [%p]", static_cast<void*>(blockedOn))); + } + QM_LOG((" mBlockedOn:%s", blockedOnString.get())); + + QM_LOG((" mExclusive: %d", mExclusive)); + + QM_LOG((" mInternal: %d", mInternal)); + + QM_LOG((" mInvalidated: %d", static_cast<bool>(mInvalidated))); + + for (auto blockedOn : mBlockedOn) { + blockedOn->Log(); + } +} + +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 +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, kProfileDoChangeTopic); + obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); + obs->RemoveObserver(this, PROFILE_BEFORE_CHANGE_QM_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, 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([&]() { return mShutdownComplete; })); + + gBasePath = nullptr; + + gStorageName = nullptr; + + gBuildId = nullptr; + + Telemetry::SetEventRecordingEnabled("dom.quota.try"_ns, false); + + 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 object + ******************************************************************************/ + +void QuotaObject::AddRef() { + QuotaManager* quotaManager = QuotaManager::Get(); + if (!quotaManager) { + NS_ERROR("Null quota manager, this shouldn't happen, possible leak!"); + + ++mRefCnt; + + return; + } + + MutexAutoLock lock(quotaManager->mQuotaMutex); + + ++mRefCnt; +} + +void QuotaObject::Release() { + QuotaManager* quotaManager = QuotaManager::Get(); + if (!quotaManager) { + NS_ERROR("Null quota manager, this shouldn't happen, possible leak!"); + + nsrefcnt count = --mRefCnt; + if (count == 0) { + mRefCnt = 1; + delete this; + } + + return; + } + + { + MutexAutoLock lock(quotaManager->mQuotaMutex); + + --mRefCnt; + + if (mRefCnt > 0) { + return; + } + + if (mOriginInfo) { + mOriginInfo->mQuotaObjects.Remove(mPath); + } + } + + delete this; +} + +bool QuotaObject::MaybeUpdateSize(int64_t aSize, bool aTruncate) { + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + MutexAutoLock lock(quotaManager->mQuotaMutex); + + return LockedMaybeUpdateSize(aSize, aTruncate); +} + +bool QuotaObject::IncreaseSize(int64_t aDelta) { + MOZ_ASSERT(aDelta >= 0); + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + MutexAutoLock lock(quotaManager->mQuotaMutex); + + AssertNoOverflow(mSize, aDelta); + int64_t size = mSize + aDelta; + + return LockedMaybeUpdateSize(size, /* aTruncate */ false); +} + +void QuotaObject::DisableQuotaCheck() { + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + MutexAutoLock lock(quotaManager->mQuotaMutex); + + mQuotaCheckDisabled = true; +} + +void QuotaObject::EnableQuotaCheck() { + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + MutexAutoLock lock(quotaManager->mQuotaMutex); + + mQuotaCheckDisabled = false; +} + +bool QuotaObject::LockedMaybeUpdateSize(int64_t aSize, bool aTruncate) { + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + quotaManager->mQuotaMutex.AssertCurrentThreadOwns(); + + if (mWritingDone == false && mOriginInfo) { + mWritingDone = true; + StorageActivityService::SendActivity(mOriginInfo->mOrigin); + } + + if (mQuotaCheckDisabled) { + return true; + } + + if (mSize == aSize) { + return true; + } + + if (!mOriginInfo) { + mSize = aSize; + return true; + } + + GroupInfo* groupInfo = mOriginInfo->mGroupInfo; + MOZ_ASSERT(groupInfo); + + if (mSize > aSize) { + if (aTruncate) { + const int64_t delta = mSize - aSize; + + AssertNoUnderflow(quotaManager->mTemporaryStorageUsage, delta); + quotaManager->mTemporaryStorageUsage -= delta; + + if (!mOriginInfo->LockedPersisted()) { + AssertNoUnderflow(groupInfo->mUsage, delta); + groupInfo->mUsage -= delta; + } + + AssertNoUnderflow(mOriginInfo->mUsage, delta); + mOriginInfo->mUsage -= delta; + + MOZ_ASSERT(mOriginInfo->mClientUsages[mClientType].isSome()); + AssertNoUnderflow(mOriginInfo->mClientUsages[mClientType].value(), delta); + mOriginInfo->mClientUsages[mClientType] = + Some(mOriginInfo->mClientUsages[mClientType].value() - delta); + + mSize = aSize; + } + return true; + } + + MOZ_ASSERT(mSize < aSize); + + RefPtr<GroupInfo> complementaryGroupInfo = + groupInfo->mGroupInfoPair->LockedGetGroupInfo( + ComplementaryPersistenceType(groupInfo->mPersistenceType)); + + uint64_t delta = aSize - mSize; + + AssertNoOverflow(mOriginInfo->mClientUsages[mClientType].valueOr(0), delta); + uint64_t newClientUsage = + mOriginInfo->mClientUsages[mClientType].valueOr(0) + delta; + + AssertNoOverflow(mOriginInfo->mUsage, delta); + uint64_t newUsage = mOriginInfo->mUsage + delta; + + // Temporary storage has no limit for origin usage (there's a group and the + // global limit though). + + uint64_t newGroupUsage = groupInfo->mUsage; + if (!mOriginInfo->LockedPersisted()) { + AssertNoOverflow(groupInfo->mUsage, delta); + newGroupUsage += delta; + + uint64_t groupUsage = groupInfo->mUsage; + if (complementaryGroupInfo) { + AssertNoOverflow(groupUsage, complementaryGroupInfo->mUsage); + groupUsage += complementaryGroupInfo->mUsage; + } + + // Temporary storage has a hard limit for group usage (20 % of the global + // limit). + AssertNoOverflow(groupUsage, delta); + if (groupUsage + delta > quotaManager->GetGroupLimit()) { + return false; + } + } + + AssertNoOverflow(quotaManager->mTemporaryStorageUsage, delta); + uint64_t newTemporaryStorageUsage = + quotaManager->mTemporaryStorageUsage + delta; + + if (newTemporaryStorageUsage > quotaManager->mTemporaryStorageLimit) { + // This will block the thread without holding the lock while waitting. + + AutoTArray<RefPtr<DirectoryLockImpl>, 10> locks; + uint64_t sizeToBeFreed; + + if (IsOnBackgroundThread()) { + MutexAutoUnlock autoUnlock(quotaManager->mQuotaMutex); + + sizeToBeFreed = quotaManager->CollectOriginsForEviction(delta, locks); + } else { + sizeToBeFreed = + quotaManager->LockedCollectOriginsForEviction(delta, locks); + } + + if (!sizeToBeFreed) { + uint64_t usage = quotaManager->mTemporaryStorageUsage; + + MutexAutoUnlock autoUnlock(quotaManager->mQuotaMutex); + + quotaManager->NotifyStoragePressure(usage); + + return false; + } + + NS_ASSERTION(sizeToBeFreed >= delta, "Huh?"); + + { + MutexAutoUnlock autoUnlock(quotaManager->mQuotaMutex); + + for (RefPtr<DirectoryLockImpl>& lock : locks) { + quotaManager->DeleteFilesForOrigin(lock->GetPersistenceType(), + lock->Origin()); + } + } + + // Relocked. + + NS_ASSERTION(mOriginInfo, "How come?!"); + + for (DirectoryLockImpl* lock : locks) { + MOZ_ASSERT(!(lock->GetPersistenceType() == groupInfo->mPersistenceType && + lock->Origin() == mOriginInfo->mOrigin), + "Deleted itself!"); + + quotaManager->LockedRemoveQuotaForOrigin(lock->GetPersistenceType(), + lock->GroupAndOrigin()); + } + + // We unlocked and relocked several times so we need to recompute all the + // essential variables and recheck the group limit. + + AssertNoUnderflow(aSize, mSize); + delta = aSize - mSize; + + AssertNoOverflow(mOriginInfo->mClientUsages[mClientType].valueOr(0), delta); + newClientUsage = mOriginInfo->mClientUsages[mClientType].valueOr(0) + delta; + + AssertNoOverflow(mOriginInfo->mUsage, delta); + newUsage = mOriginInfo->mUsage + delta; + + newGroupUsage = groupInfo->mUsage; + if (!mOriginInfo->LockedPersisted()) { + AssertNoOverflow(groupInfo->mUsage, delta); + newGroupUsage += delta; + + uint64_t groupUsage = groupInfo->mUsage; + if (complementaryGroupInfo) { + AssertNoOverflow(groupUsage, complementaryGroupInfo->mUsage); + groupUsage += complementaryGroupInfo->mUsage; + } + + AssertNoOverflow(groupUsage, delta); + if (groupUsage + delta > quotaManager->GetGroupLimit()) { + // Unfortunately some other thread increased the group usage in the + // meantime and we are not below the group limit anymore. + + // However, the origin eviction must be finalized in this case too. + MutexAutoUnlock autoUnlock(quotaManager->mQuotaMutex); + + quotaManager->FinalizeOriginEviction(std::move(locks)); + + return false; + } + } + + AssertNoOverflow(quotaManager->mTemporaryStorageUsage, delta); + newTemporaryStorageUsage = quotaManager->mTemporaryStorageUsage + delta; + + NS_ASSERTION( + newTemporaryStorageUsage <= quotaManager->mTemporaryStorageLimit, + "How come?!"); + + // Ok, we successfully freed enough space and the operation can continue + // without throwing the quota error. + mOriginInfo->mClientUsages[mClientType] = Some(newClientUsage); + + mOriginInfo->mUsage = newUsage; + if (!mOriginInfo->LockedPersisted()) { + groupInfo->mUsage = newGroupUsage; + } + quotaManager->mTemporaryStorageUsage = newTemporaryStorageUsage; + ; + + // Some other thread could increase the size in the meantime, but no more + // than this one. + MOZ_ASSERT(mSize < aSize); + mSize = aSize; + + // Finally, release IO thread only objects and allow next synchronized + // ops for the evicted origins. + MutexAutoUnlock autoUnlock(quotaManager->mQuotaMutex); + + quotaManager->FinalizeOriginEviction(std::move(locks)); + + return true; + } + + mOriginInfo->mClientUsages[mClientType] = Some(newClientUsage); + + mOriginInfo->mUsage = newUsage; + if (!mOriginInfo->LockedPersisted()) { + groupInfo->mUsage = newGroupUsage; + } + quotaManager->mTemporaryStorageUsage = newTemporaryStorageUsage; + + mSize = aSize; + + return true; +} + +/******************************************************************************* + * Quota manager + ******************************************************************************/ + +QuotaManager::QuotaManager(const nsAString& aBasePath, + const nsAString& aStorageName) + : mQuotaMutex("QuotaManager.mQuotaMutex"), + mBasePath(aBasePath), + mStorageName(aStorageName), + mTemporaryStorageUsage(0), + mNextDirectoryLockId(0), + mTemporaryStorageInitialized(false), + mCacheUsable(false) { + AssertIsOnOwningThread(); + MOZ_ASSERT(!gInstance); +} + +QuotaManager::~QuotaManager() { + AssertIsOnOwningThread(); + MOZ_ASSERT(!gInstance || gInstance == this); +} + +// static +nsresult QuotaManager::Initialize() { + MOZ_ASSERT(NS_IsMainThread()); + +#ifdef QM_ENABLE_SCOPED_LOG_EXTRA_INFO + ScopedLogExtraInfo::Initialize(); +#endif + + nsresult rv = Observer::Initialize(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +void QuotaManager::GetOrCreate(nsIRunnable* aCallback, + nsIEventTarget* aMainEventTarget) { + AssertIsOnBackgroundThread(); + + if (IsShuttingDown()) { + MOZ_ASSERT(false, "Calling QuotaManager::GetOrCreate() after shutdown!"); + return; + } + + if (NS_WARN_IF(!gBasePath)) { + NS_WARNING("profile-do-change must precede QuotaManager::GetOrCreate()"); + MOZ_ASSERT(!gInstance); + } else if (gInstance || gCreateFailed) { + MOZ_ASSERT_IF(gCreateFailed, !gInstance); + } else { + RefPtr<QuotaManager> manager = new QuotaManager(*gBasePath, *gStorageName); + + nsresult rv = manager->Init(); + if (NS_WARN_IF(NS_FAILED(rv))) { + gCreateFailed = true; + } else { + gInstance = manager; + } + } + + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(aCallback)); +} + +// static +QuotaManager* QuotaManager::Get() { + // Does not return an owning reference. + return gInstance; +} + +// static +QuotaManager& QuotaManager::GetRef() { + MOZ_ASSERT(gInstance); + + return *gInstance; +} + +// static +bool QuotaManager::IsShuttingDown() { return gShutdown; } + +// static +void QuotaManager::ShutdownInstance() { + AssertIsOnBackgroundThread(); + + if (gInstance) { + gInstance->Shutdown(); + + gInstance = nullptr; + } + + RefPtr<Runnable> runnable = + NS_NewRunnableFunction("dom::quota::QuotaManager::ShutdownCompleted", + []() { Observer::ShutdownCompleted(); }); + MOZ_ASSERT(runnable); + + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(runnable.forget())); +} + +// static +bool QuotaManager::IsOSMetadata(const nsAString& aFileName) { + return aFileName.EqualsLiteral(DSSTORE_FILE_NAME) || + aFileName.EqualsLiteral(DESKTOP_FILE_NAME) || + aFileName.LowerCaseEqualsLiteral(DESKTOP_INI_FILE_NAME) || + aFileName.LowerCaseEqualsLiteral(THUMBS_DB_FILE_NAME); +} + +// static +bool QuotaManager::IsDotFile(const nsAString& aFileName) { + return aFileName.First() == char16_t('.'); +} + +auto QuotaManager::CreateDirectoryLock( + const Nullable<PersistenceType>& aPersistenceType, const nsACString& aGroup, + const OriginScope& aOriginScope, const Nullable<Client::Type>& aClientType, + bool aExclusive, bool aInternal, + RefPtr<OpenDirectoryListener> aOpenListener, bool& aBlockedOut) + -> already_AddRefed<DirectoryLockImpl> { + AssertIsOnOwningThread(); + MOZ_ASSERT_IF(aOriginScope.IsOrigin(), !aOriginScope.GetOrigin().IsEmpty()); + MOZ_ASSERT_IF(!aInternal, !aPersistenceType.IsNull()); + MOZ_ASSERT_IF(!aInternal, + aPersistenceType.Value() != PERSISTENCE_TYPE_INVALID); + MOZ_ASSERT_IF(!aInternal, !aGroup.IsEmpty()); + MOZ_ASSERT_IF(!aInternal, aOriginScope.IsOrigin()); + MOZ_ASSERT_IF(!aInternal, !aClientType.IsNull()); + MOZ_ASSERT_IF(!aInternal, aClientType.Value() < Client::TypeMax()); + MOZ_ASSERT_IF(!aInternal, aOpenListener); + + RefPtr<DirectoryLockImpl> lock = new DirectoryLockImpl( + WrapNotNullUnchecked(this), GenerateDirectoryLockId(), aPersistenceType, + aGroup, aOriginScope, aClientType, aExclusive, aInternal, + ShouldUpdateLockIdTableFlag::Yes, std::move(aOpenListener)); + + mPendingDirectoryLocks.AppendElement(lock); + + // See if this lock needs to wait. + bool blocked = false; + + // XXX It is probably unnecessary to iterate this in reverse order. + for (DirectoryLockImpl* const existingLock : Reversed(mDirectoryLocks)) { + if (lock->MustWaitFor(*existingLock)) { + existingLock->AddBlockingLock(*lock); + lock->AddBlockedOnLock(*existingLock); + blocked = true; + } + } + + RegisterDirectoryLock(*lock); + + // Otherwise, notify the open listener immediately. + if (!blocked) { + lock->NotifyOpenListener(); + } + + aBlockedOut = blocked; + return lock.forget(); +} + +auto QuotaManager::CreateDirectoryLockForEviction( + PersistenceType aPersistenceType, const GroupAndOrigin& aGroupAndOrigin) + -> already_AddRefed<DirectoryLockImpl> { + AssertIsOnOwningThread(); + MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_INVALID); + MOZ_ASSERT(!aGroupAndOrigin.mOrigin.IsEmpty()); + + RefPtr<DirectoryLockImpl> lock = new DirectoryLockImpl( + WrapNotNullUnchecked(this), GenerateDirectoryLockId(), + Nullable<PersistenceType>(aPersistenceType), aGroupAndOrigin.mGroup, + OriginScope::FromOrigin(aGroupAndOrigin.mOrigin), + Nullable<Client::Type>(), + /* aExclusive */ true, /* aInternal */ true, + ShouldUpdateLockIdTableFlag::No, nullptr); + +#ifdef DEBUG + for (const DirectoryLockImpl* const existingLock : mDirectoryLocks) { + MOZ_ASSERT(!lock->MustWaitFor(*existingLock)); + } +#endif + + RegisterDirectoryLock(*lock); + + return lock.forget(); +} + +void QuotaManager::RegisterDirectoryLock(DirectoryLockImpl& aLock) { + AssertIsOnOwningThread(); + + mDirectoryLocks.AppendElement(&aLock); + + if (aLock.ShouldUpdateLockIdTable()) { + MutexAutoLock lock(mQuotaMutex); + + MOZ_DIAGNOSTIC_ASSERT(!mDirectoryLockIdTable.Get(aLock.Id())); + mDirectoryLockIdTable.Put(aLock.Id(), &aLock); + } + + if (aLock.ShouldUpdateLockTable()) { + DirectoryLockTable& directoryLockTable = + GetDirectoryLockTable(aLock.GetPersistenceType()); + + nsTArray<NotNull<DirectoryLockImpl*>>* array; + if (!directoryLockTable.Get(aLock.Origin(), &array)) { + array = new nsTArray<NotNull<DirectoryLockImpl*>>(); + directoryLockTable.Put(aLock.Origin(), array); + + if (!IsShuttingDown()) { + UpdateOriginAccessTime(aLock.GetPersistenceType(), + aLock.GroupAndOrigin()); + } + } + + // 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 nsDataHashtable<nsCStringHashKey, uint32_t>? + array->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.Get(aLock.Id())); + mDirectoryLockIdTable.Remove(aLock.Id()); + } + + if (aLock.ShouldUpdateLockTable()) { + DirectoryLockTable& directoryLockTable = + GetDirectoryLockTable(aLock.GetPersistenceType()); + + nsTArray<NotNull<DirectoryLockImpl*>>* array; + MOZ_ALWAYS_TRUE(directoryLockTable.Get(aLock.Origin(), &array)); + + MOZ_ALWAYS_TRUE(array->RemoveElement(&aLock)); + if (array->IsEmpty()) { + directoryLockTable.Remove(aLock.Origin()); + + if (!IsShuttingDown()) { + UpdateOriginAccessTime(aLock.GetPersistenceType(), + aLock.GroupAndOrigin()); + } + } + } + + aLock.SetRegistered(false); +} + +void QuotaManager::RemovePendingDirectoryLock(DirectoryLockImpl& aLock) { + AssertIsOnOwningThread(); + + MOZ_ALWAYS_TRUE(mPendingDirectoryLocks.RemoveElement(&aLock)); +} + +uint64_t QuotaManager::CollectOriginsForEviction( + uint64_t aMinSizeToBeFreed, nsTArray<RefPtr<DirectoryLockImpl>>& aLocks) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aLocks.IsEmpty()); + + struct MOZ_STACK_CLASS Helper final { + static void GetInactiveOriginInfos( + const nsTArray<NotNull<RefPtr<OriginInfo>>>& aOriginInfos, + const nsTArray<DirectoryLockImpl*>& aLocks, + nsTArray<NotNull<RefPtr<OriginInfo>>>& aInactiveOriginInfos) { + for (const NotNull<RefPtr<OriginInfo>>& originInfo : aOriginInfos) { + MOZ_ASSERT(originInfo->mGroupInfo->mPersistenceType != + PERSISTENCE_TYPE_PERSISTENT); + + if (originInfo->LockedPersisted()) { + 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->mQuotaObjects.Count(), + "Inactive origin shouldn't have open files!"); + aInactiveOriginInfos.InsertElementSorted(originInfo, + OriginInfoLRUComparator()); + } + } + } + }; + + // Split locks into separate arrays and filter out locks for persistent + // storage, they can't block us. + nsTArray<DirectoryLockImpl*> temporaryStorageLocks; + nsTArray<DirectoryLockImpl*> defaultStorageLocks; + for (DirectoryLockImpl* 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 { + MOZ_ASSERT(persistenceType.Value() == PERSISTENCE_TYPE_PERSISTENT); + + // Do nothing here, persistent origins don't need to be collected ever. + } + } + + nsTArray<NotNull<RefPtr<OriginInfo>>> inactiveOrigins; + + // Enumerate and process inactive origins. This must be protected by the + // mutex. + 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) { + Helper::GetInactiveOriginInfos(groupInfo->mOriginInfos, + temporaryStorageLocks, inactiveOrigins); + } + + groupInfo = pair->LockedGetGroupInfo(PERSISTENCE_TYPE_DEFAULT); + if (groupInfo) { + Helper::GetInactiveOriginInfos(groupInfo->mOriginInfos, + defaultStorageLocks, 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(); + } + + 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) { + RefPtr<DirectoryLockImpl> lock = CreateDirectoryLockForEviction( + originInfo->mGroupInfo->mPersistenceType, + GroupAndOrigin{originInfo->mGroupInfo->mGroup, originInfo->mOrigin}); + aLocks.AppendElement(lock.forget()); + } + + return sizeToBeFreed; + } + + return 0; +} + +template <typename P> +void QuotaManager::CollectPendingOriginsForListing(P aPredicate) { + MutexAutoLock lock(mQuotaMutex); + + for (const auto& entry : mGroupInfoPairs) { + const auto& pair = entry.GetData(); + + MOZ_ASSERT(!entry.GetKey().IsEmpty()); + MOZ_ASSERT(pair); + + RefPtr<GroupInfo> groupInfo = + pair->LockedGetGroupInfo(PERSISTENCE_TYPE_DEFAULT); + if (groupInfo) { + for (const auto& originInfo : groupInfo->mOriginInfos) { + if (!originInfo->mDirectoryExists) { + aPredicate(originInfo); + } + } + } + } +} + +nsresult QuotaManager::Init() { + AssertIsOnOwningThread(); + +#ifdef XP_WIN + CacheUseDOSDevicePathSyntaxPrefValue(); +#endif + + QM_TRY_INSPECT(const auto& baseDir, QM_NewLocalFile(mBasePath)); + + QM_TRY_UNWRAP( + do_Init(mIndexedDBPath), + GetPathForStorage(*baseDir, nsLiteralString(INDEXEDDB_DIRECTORY_NAME))); + + QM_TRY(baseDir->Append(mStorageName)); + + QM_TRY_UNWRAP(do_Init(mStoragePath), + MOZ_TO_RESULT_INVOKE_TYPED(nsString, baseDir, GetPath)); + + 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(mIOThread), + ToResultInvoke<nsCOMPtr<nsIThread>>( + MOZ_SELECT_OVERLOAD(NS_NewNamedThread), "QuotaManager IO")); + + // Make a timer here to avoid potential failures later. We don't actually + // initialize the timer until shutdown. + nsCOMPtr shutdownTimer = NS_NewTimer(); + QM_TRY(OkIf(shutdownTimer), Err(NS_ERROR_FAILURE)); + + mShutdownTimer.init(WrapNotNullUnchecked(std::move(shutdownTimer))); + + static_assert(Client::IDB == 0 && Client::DOMCACHE == 1 && Client::SDB == 2 && + Client::LS == 3 && Client::TYPE_MAX == 4, + "Fix the registration!"); + + // Register clients. + auto clients = decltype(mClients)::ValueType{}; + clients.AppendElement(indexedDB::CreateQuotaClient()); + clients.AppendElement(cache::CreateQuotaClient()); + clients.AppendElement(simpledb::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::LS}); + mAllClientTypesExceptLS.init(ClientTypesArray{ + Client::Type::IDB, Client::Type::DOMCACHE, Client::Type::SDB}); + + return NS_OK; +} + +void QuotaManager::MaybeRecordShutdownStep(const Client::Type aClientType, + const nsACString& aStepDescription) { + // Callable on any thread. + + MaybeRecordShutdownStep(Some(aClientType), aStepDescription); +} + +void QuotaManager::MaybeRecordQuotaManagerShutdownStep( + const nsACString& aStepDescription) { + // Callable on any thread. + + MaybeRecordShutdownStep(Nothing{}, aStepDescription); +} + +void QuotaManager::MaybeRecordShutdownStep( + const Maybe<Client::Type> aClientType, const nsACString& aStepDescription) { + if (!mShutdownStarted) { + // We are not shutting down yet, we intentionally ignore this here to avoid + // that every caller has to make a distinction for shutdown vs. non-shutdown + // situations. + return; + } + + 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_ASSERT(!mShutdownStarted); + + // Setting this flag prevents the service from being recreated and prevents + // further storagess from being created. + if (gShutdown.exchange(true)) { + NS_ERROR("Shutdown more than once?!"); + } + + StopIdleMaintenance(); + + mShutdownStartedAt.init(TimeStamp::NowLoRes()); + mShutdownStarted = true; + + const auto& allClientTypes = AllClientTypes(); + + bool needsToWait = false; + for (Client::Type type : allClientTypes) { + needsToWait |= (*mClients)[type]->InitiateShutdownWorkThreads(); + } + needsToWait |= static_cast<bool>(gNormalOriginOps); + + // If any clients cannot shutdown immediately, spin the event loop while we + // wait on all the threads to close. Our timer may fire during that loop. + if (needsToWait) { + MOZ_ALWAYS_SUCCEEDS( + (*mShutdownTimer) + ->InitWithNamedFuncCallback( + [](nsITimer* aTimer, void* aClosure) { + auto* const quotaManager = + static_cast<QuotaManager*>(aClosure); + + for (Client::Type type : quotaManager->AllClientTypes()) { + // XXX This is a workaround to unblock shutdown, which ought + // to be removed by Bug 1682326. + if (type == Client::IDB) { + (*quotaManager->mClients)[type]->AbortAllOperations(); + } + + (*quotaManager->mClients)[type]->ForceKillActors(); + } + + MOZ_ALWAYS_SUCCEEDS(aTimer->InitWithNamedFuncCallback( + [](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) { + MutexAutoLock lock(quotaManager->mQuotaMutex); + + annotation.AppendPrintf( + "QM: %zu normal origin ops " + "pending\nIntermediate " + "steps:\n%s\n", + gNormalOriginOps->Length(), + quotaManager->mQuotaManagerShutdownSteps.get()); + } + } + + // We expect that at least one quota client didn't + // complete its shutdown. + MOZ_DIAGNOSTIC_ASSERT(!annotation.IsEmpty()); + + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation:: + QuotaManagerShutdownTimeout, + annotation); + + MOZ_CRASH("Quota manager shutdown timed out"); + }, + aClosure, SHUTDOWN_FORCE_CRASH_TIMEOUT_MS, + nsITimer::TYPE_ONE_SHOT, + "quota::QuotaManager::ForceCrashTimer")); + }, + this, SHUTDOWN_FORCE_KILL_TIMEOUT_MS, nsITimer::TYPE_ONE_SHOT, + "quota::QuotaManager::ForceKillTimer")); + + MOZ_ALWAYS_TRUE(SpinEventLoopUntil([this, &allClientTypes] { + return !gNormalOriginOps && + std::all_of(allClientTypes.cbegin(), allClientTypes.cend(), + [&self = *this](const auto type) { + return (*self.mClients)[type]->IsShutdownCompleted(); + }); + })); + } + + for (Client::Type type : allClientTypes) { + (*mClients)[type]->FinalizeShutdownWorkThreads(); + } + + // Cancel the timer regardless of whether it actually fired. + if (NS_FAILED((*mShutdownTimer)->Cancel())) { + NS_WARNING("Failed to cancel shutdown timer!"); + } + + // NB: It's very important that runnable is destroyed on this thread + // (i.e. after we join the IO thread) because we can't release the + // QuotaManager on the IO thread. This should probably use + // NewNonOwningRunnableMethod ... + RefPtr<Runnable> runnable = + NewRunnableMethod("dom::quota::QuotaManager::ShutdownStorage", this, + &QuotaManager::ShutdownStorage); + MOZ_ASSERT(runnable); + + // Give clients a chance to cleanup IO thread only objects. + if (NS_FAILED((*mIOThread)->Dispatch(runnable, NS_DISPATCH_NORMAL))) { + NS_WARNING("Failed to dispatch runnable!"); + } + + // Make sure to join with our IO thread. + if (NS_FAILED((*mIOThread)->Shutdown())) { + NS_WARNING("Failed to shutdown IO thread!"); + } + + for (RefPtr<DirectoryLockImpl>& lock : mPendingDirectoryLocks) { + lock->Invalidate(); + } +} + +void QuotaManager::InitQuotaForOrigin(PersistenceType aPersistenceType, + const GroupAndOrigin& aGroupAndOrigin, + const ClientUsageArray& aClientUsages, + uint64_t aUsageBytes, int64_t aAccessTime, + bool aPersisted) { + AssertIsOnIOThread(); + MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT); + + MutexAutoLock lock(mQuotaMutex); + + RefPtr<GroupInfo> groupInfo = + LockedGetOrCreateGroupInfo(aPersistenceType, aGroupAndOrigin.mGroup); + + groupInfo->LockedAddOriginInfo(MakeNotNull<RefPtr<OriginInfo>>( + groupInfo, aGroupAndOrigin.mOrigin, aClientUsages, aUsageBytes, + aAccessTime, aPersisted, + /* aDirectoryExists */ true)); +} + +void QuotaManager::EnsureQuotaForOrigin(PersistenceType aPersistenceType, + const GroupAndOrigin& aGroupAndOrigin) { + AssertIsOnIOThread(); + MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT); + + MutexAutoLock lock(mQuotaMutex); + + RefPtr<GroupInfo> groupInfo = + LockedGetOrCreateGroupInfo(aPersistenceType, aGroupAndOrigin.mGroup); + + RefPtr<OriginInfo> originInfo = + groupInfo->LockedGetOriginInfo(aGroupAndOrigin.mOrigin); + if (!originInfo) { + groupInfo->LockedAddOriginInfo(MakeNotNull<RefPtr<OriginInfo>>( + groupInfo, aGroupAndOrigin.mOrigin, ClientUsageArray(), + /* aUsageBytes */ 0, + /* aAccessTime */ PR_Now(), /* aPersisted */ false, + /* aDirectoryExists */ false)); + } +} + +void QuotaManager::NoteOriginDirectoryCreated( + PersistenceType aPersistenceType, const GroupAndOrigin& aGroupAndOrigin, + bool aPersisted, int64_t& aTimestamp) { + AssertIsOnIOThread(); + MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT); + + int64_t timestamp; + + MutexAutoLock lock(mQuotaMutex); + + RefPtr<GroupInfo> groupInfo = + LockedGetOrCreateGroupInfo(aPersistenceType, aGroupAndOrigin.mGroup); + + RefPtr<OriginInfo> originInfo = + groupInfo->LockedGetOriginInfo(aGroupAndOrigin.mOrigin); + if (originInfo) { + originInfo->mPersisted = aPersisted; + originInfo->mDirectoryExists = true; + timestamp = originInfo->LockedAccessTime(); + } else { + timestamp = PR_Now(); + groupInfo->LockedAddOriginInfo(MakeNotNull<RefPtr<OriginInfo>>( + groupInfo, aGroupAndOrigin.mOrigin, ClientUsageArray(), + /* aUsageBytes */ 0, + /* aAccessTime */ timestamp, aPersisted, /* aDirectoryExists */ true)); + } + + aTimestamp = timestamp; +} + +void QuotaManager::DecreaseUsageForOrigin(PersistenceType aPersistenceType, + const GroupAndOrigin& aGroupAndOrigin, + Client::Type aClientType, + int64_t aSize) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT); + + MutexAutoLock lock(mQuotaMutex); + + GroupInfoPair* pair; + if (!mGroupInfoPairs.Get(aGroupAndOrigin.mGroup, &pair)) { + return; + } + + RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(aPersistenceType); + if (!groupInfo) { + return; + } + + RefPtr<OriginInfo> originInfo = + groupInfo->LockedGetOriginInfo(aGroupAndOrigin.mOrigin); + if (originInfo) { + originInfo->LockedDecreaseUsage(aClientType, aSize); + } +} + +void QuotaManager::ResetUsageForClient(PersistenceType aPersistenceType, + const GroupAndOrigin& aGroupAndOrigin, + Client::Type aClientType) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT); + + MutexAutoLock lock(mQuotaMutex); + + GroupInfoPair* pair; + if (!mGroupInfoPairs.Get(aGroupAndOrigin.mGroup, &pair)) { + return; + } + + RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(aPersistenceType); + if (!groupInfo) { + return; + } + + RefPtr<OriginInfo> originInfo = + groupInfo->LockedGetOriginInfo(aGroupAndOrigin.mOrigin); + if (originInfo) { + originInfo->LockedResetUsageForClient(aClientType); + } +} + +UsageInfo QuotaManager::GetUsageForClient(PersistenceType aPersistenceType, + const GroupAndOrigin& aGroupAndOrigin, + Client::Type aClientType) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT); + + MutexAutoLock lock(mQuotaMutex); + + GroupInfoPair* pair; + if (!mGroupInfoPairs.Get(aGroupAndOrigin.mGroup, &pair)) { + return UsageInfo{}; + } + + RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(aPersistenceType); + if (!groupInfo) { + return UsageInfo{}; + } + + RefPtr<OriginInfo> originInfo = + groupInfo->LockedGetOriginInfo(aGroupAndOrigin.mOrigin); + if (!originInfo) { + return UsageInfo{}; + } + + return originInfo->LockedGetUsageForClient(aClientType); +} + +void QuotaManager::UpdateOriginAccessTime( + PersistenceType aPersistenceType, const GroupAndOrigin& aGroupAndOrigin) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT); + MOZ_ASSERT(!IsShuttingDown()); + + MutexAutoLock lock(mQuotaMutex); + + GroupInfoPair* pair; + if (!mGroupInfoPairs.Get(aGroupAndOrigin.mGroup, &pair)) { + return; + } + + RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(aPersistenceType); + if (!groupInfo) { + return; + } + + RefPtr<OriginInfo> originInfo = + groupInfo->LockedGetOriginInfo(aGroupAndOrigin.mOrigin); + if (originInfo) { + int64_t timestamp = PR_Now(); + originInfo->LockedUpdateAccessTime(timestamp); + + MutexAutoUnlock autoUnlock(mQuotaMutex); + + auto op = MakeRefPtr<SaveOriginAccessTimeOp>( + aPersistenceType, aGroupAndOrigin.mOrigin, 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(); + } + } + + mGroupInfoPairs.Clear(); + + MOZ_ASSERT(mTemporaryStorageUsage == 0, "Should be zero!"); +} + +nsresult QuotaManager::LoadQuota() { + AssertIsOnIOThread(); + MOZ_ASSERT(mStorageConnection); + MOZ_ASSERT(!mTemporaryStorageInitialized); + + auto recordQuotaInfoLoadTimeHelper = + MakeRefPtr<RecordQuotaInfoLoadTimeHelper>(); + recordQuotaInfoLoadTimeHelper->Start(); + + auto LoadQuotaFromCache = [&]() -> nsresult { + QM_TRY_INSPECT( + const auto& stmt, + MOZ_TO_RESULT_INVOKE_TYPED( + nsCOMPtr<mozIStorageStatement>, mStorageConnection, CreateStatement, + "SELECT repository_id, origin, group_, client_usages, usage, " + "last_access_time, accessed, persisted " + "FROM origin"_ns)); + + auto autoRemoveQuota = MakeScopeExit([&] { RemoveQuota(); }); + + QM_TRY(quota::CollectWhileHasResult( + *stmt, [this](auto& stmt) -> Result<Ok, nsresult> { + QM_TRY_INSPECT(const int32_t& repositoryId, + MOZ_TO_RESULT_INVOKE(stmt, GetInt32, 0)); + + const auto maybePersistenceType = + PersistenceTypeFromInt32(repositoryId, fallible); + QM_TRY(OkIf(maybePersistenceType.isSome()), Err(NS_ERROR_FAILURE)); + + const PersistenceType persistenceType = maybePersistenceType.value(); + + GroupAndOrigin groupAndOrigin; + + QM_TRY_UNWRAP( + groupAndOrigin.mOrigin, + MOZ_TO_RESULT_INVOKE_TYPED(nsCString, stmt, GetUTF8String, 1)); + + QM_TRY_UNWRAP( + groupAndOrigin.mGroup, + MOZ_TO_RESULT_INVOKE_TYPED(nsCString, stmt, GetUTF8String, 2)); + + QM_TRY_INSPECT(const bool& updated, + MaybeUpdateGroupForOrigin(groupAndOrigin)); + + Unused << updated; + + // 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 before initializing quota for the + // given origin. (Note that calling GetDirectoryMetadata2WithRestore + // below 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& clientUsagesText, + MOZ_TO_RESULT_INVOKE_TYPED(nsCString, stmt, GetUTF8String, 3)); + + ClientUsageArray clientUsages; + QM_TRY(clientUsages.Deserialize(clientUsagesText)); + + QM_TRY_INSPECT(const int64_t& usage, + MOZ_TO_RESULT_INVOKE(stmt, GetInt64, 4)); + QM_TRY_INSPECT(const int64_t& lastAccessTime, + MOZ_TO_RESULT_INVOKE(stmt, GetInt64, 5)); + QM_TRY_INSPECT(const int64_t& accessed, + MOZ_TO_RESULT_INVOKE(stmt, GetInt32, 6)); + QM_TRY_INSPECT(const int64_t& persisted, + MOZ_TO_RESULT_INVOKE(stmt, GetInt32, 7)); + + if (accessed) { + QM_TRY_INSPECT( + const auto& directory, + GetDirectoryForOrigin(persistenceType, groupAndOrigin.mOrigin)); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE(directory, Exists)); + + QM_TRY(OkIf(exists), Err(NS_ERROR_FAILURE)); + + QM_TRY_INSPECT(const bool& isDirectory, + MOZ_TO_RESULT_INVOKE(directory, IsDirectory)); + + QM_TRY(OkIf(isDirectory), Err(NS_ERROR_FAILURE)); + + // Calling GetDirectoryMetadata2WithRestore 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, + GetDirectoryMetadataWithQuotaInfo2WithRestore( + directory, /* aPersistent */ false)); + + QM_TRY(OkIf(lastAccessTime == metadata.mTimestamp), + Err(NS_ERROR_FAILURE)); + + QM_TRY(OkIf(persisted == metadata.mPersisted), + Err(NS_ERROR_FAILURE)); + + QM_TRY(OkIf(groupAndOrigin.mGroup == metadata.mQuotaInfo.mGroup), + Err(NS_ERROR_FAILURE)); + + QM_TRY(OkIf(groupAndOrigin.mOrigin == metadata.mQuotaInfo.mOrigin), + Err(NS_ERROR_FAILURE)); + + QM_TRY(InitializeOrigin(persistenceType, groupAndOrigin, + lastAccessTime, persisted, directory)); + } else { + InitQuotaForOrigin(persistenceType, groupAndOrigin, clientUsages, + usage, lastAccessTime, persisted); + } + + 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(stmt, GetInt32, 0)); + + if (valid) { + QM_TRY_INSPECT(const auto& buildId, + MOZ_TO_RESULT_INVOKE_TYPED(nsAutoCString, stmt, + GetUTF8String, 1)); + + return buildId == *gBuildId; + } + } + + return false; + }())); + + auto autoRemoveQuota = MakeScopeExit([&] { RemoveQuota(); }); + + if (!loadQuotaFromCache || + !StaticPrefs::dom_quotaManager_loadQuotaFromCache() || + ![&LoadQuotaFromCache] { + QM_TRY(LoadQuotaFromCache(), false); + return true; + }()) { + // 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 : kBestEffortPersistenceTypes) { + if (NS_WARN_IF(IsShuttingDown())) { + RETURN_STATUS_OR_RESULT(statusKeeper, NS_ERROR_ABORT); + } + + QM_TRY(([&]() -> Result<Ok, nsresult> { + QM_TRY(([this, type] { + const nsresult rv = InitializeRepository(type); + mInitializationInfo.RecordFirstInitializationAttempt( + type == PERSISTENCE_TYPE_DEFAULT + ? Initialization::DefaultRepository + : Initialization::TemporaryRepository, + rv); + return rv; + }()), + OK_IN_NIGHTLY_PROPAGATE_IN_OTHERS, statusKeeperFunc); + + return Ok{}; + }())); + } + +#ifdef NIGHTLY_BUILD + if (NS_FAILED(statusKeeper)) { + return statusKeeper; + } +#endif + } + + if (mCacheUsable) { + QM_TRY(InvalidateCache(*mStorageConnection)); + } + + recordQuotaInfoLoadTimeHelper->End(); + + autoRemoveQuota.release(); + + return NS_OK; +} + +void QuotaManager::UnloadQuota() { + AssertIsOnIOThread(); + MOZ_ASSERT(mStorageConnection); + MOZ_ASSERT(mTemporaryStorageInitialized); + MOZ_ASSERT(mCacheUsable); + + auto autoRemoveQuota = MakeScopeExit([&] { RemoveQuota(); }); + + mozStorageTransaction transaction( + mStorageConnection, false, mozIStorageConnection::TRANSACTION_IMMEDIATE); + + QM_TRY(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->mQuotaObjects.Count()); + + if (!originInfo->mDirectoryExists) { + continue; + } + + if (insertStmt) { + MOZ_ALWAYS_SUCCEEDS(insertStmt->Reset()); + } else { + QM_TRY_UNWRAP( + insertStmt, + MOZ_TO_RESULT_INVOKE_TYPED( + nsCOMPtr<mozIStorageStatement>, mStorageConnection, + CreateStatement, + "INSERT INTO origin (repository_id, origin, group_, " + "client_usages, usage, last_access_time, accessed, " + "persisted) " + "VALUES (:repository_id, :origin, :group_, " + ":client_usages, " + ":usage, :last_access_time, :accessed, :persisted)"_ns), + QM_VOID); + } + + QM_TRY(originInfo->LockedBindToStatement(insertStmt), QM_VOID); + + QM_TRY(insertStmt->Execute(), QM_VOID); + } + + groupInfo->LockedRemoveOriginInfos(); + } + + iter.Remove(); + } + } + + QM_TRY_INSPECT( + const auto& stmt, + MOZ_TO_RESULT_INVOKE_TYPED( + nsCOMPtr<mozIStorageStatement>, mStorageConnection, CreateStatement, + "UPDATE cache SET valid = :valid, build_id = :buildId;"_ns), + QM_VOID); + + QM_TRY(stmt->BindInt32ByName("valid"_ns, 1), QM_VOID); + QM_TRY(stmt->BindUTF8StringByName("buildId"_ns, *gBuildId), QM_VOID); + QM_TRY(stmt->Execute(), QM_VOID); + QM_TRY(transaction.Commit(), QM_VOID); +} + +already_AddRefed<QuotaObject> QuotaManager::GetQuotaObject( + PersistenceType aPersistenceType, const GroupAndOrigin& aGroupAndOrigin, + Client::Type aClientType, nsIFile* aFile, int64_t aFileSize, + int64_t* aFileSizeOut /* = nullptr */) { + NS_ASSERTION(!NS_IsMainThread(), "Wrong thread!"); + + if (aFileSizeOut) { + *aFileSizeOut = 0; + } + + if (aPersistenceType == PERSISTENCE_TYPE_PERSISTENT) { + return nullptr; + } + + QM_TRY_INSPECT(const auto& path, + MOZ_TO_RESULT_INVOKE_TYPED(nsString, aFile, GetPath), nullptr); + +#ifdef DEBUG + { + QM_TRY_INSPECT( + const auto& directory, + GetDirectoryForOrigin(aPersistenceType, aGroupAndOrigin.mOrigin), + nullptr); + + nsAutoString clientType; + QM_TRY(OkIf(Client::TypeToText(aClientType, clientType, fallible)), + nullptr); + + QM_TRY(directory->Append(clientType), nullptr); + + QM_TRY_INSPECT(const auto& directoryPath, + MOZ_TO_RESULT_INVOKE_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(aFile, Exists)); + + if (exists) { + QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE(aFile, GetFileSize)); + } + + return 0; + } + + return aFileSize; + }()), + nullptr); + + RefPtr<QuotaObject> result; + { + MutexAutoLock lock(mQuotaMutex); + + GroupInfoPair* pair; + if (!mGroupInfoPairs.Get(aGroupAndOrigin.mGroup, &pair)) { + return nullptr; + } + + RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(aPersistenceType); + + if (!groupInfo) { + return nullptr; + } + + RefPtr<OriginInfo> originInfo = + groupInfo->LockedGetOriginInfo(aGroupAndOrigin.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. + QuotaObject* quotaObject; + if (!originInfo->mQuotaObjects.Get(path, "aObject)) { + // Create a new QuotaObject. + quotaObject = new QuotaObject(originInfo, aClientType, path, fileSize); + + // Put it to the hashtable. The hashtable is not responsible to delete + // the QuotaObject. + originInfo->mQuotaObjects.Put(path, quotaObject); + } + + // Addref the QuotaObject and move the ownership to the result. This must + // happen before we unlock! + result = quotaObject->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 GroupAndOrigin& aGroupAndOrigin, + 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, aGroupAndOrigin, 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); + } + + DirectoryLockImpl* directoryLock; + if (!mDirectoryLockIdTable.Get(aDirectoryLockId, &directoryLock)) { + MOZ_CRASH("Getting quota object for an unregistered directory lock?"); + } + MOZ_DIAGNOSTIC_ASSERT(directoryLock); + MOZ_DIAGNOSTIC_ASSERT(directoryLock->ShouldUpdateLockIdTable()); + + const PersistenceType persistenceType = directoryLock->GetPersistenceType(); + const GroupAndOrigin& groupAndOrigin = directoryLock->GroupAndOrigin(); + const Client::Type clientType = directoryLock->ClientType(); + + lock.reset(); + + return GetQuotaObject(persistenceType, groupAndOrigin, clientType, aPath); +} + +Nullable<bool> QuotaManager::OriginPersisted( + const GroupAndOrigin& aGroupAndOrigin) { + AssertIsOnIOThread(); + + MutexAutoLock lock(mQuotaMutex); + + RefPtr<OriginInfo> originInfo = + LockedGetOriginInfo(PERSISTENCE_TYPE_DEFAULT, aGroupAndOrigin); + if (originInfo) { + return Nullable<bool>(originInfo->LockedPersisted()); + } + + return Nullable<bool>(); +} + +void QuotaManager::PersistOrigin(const GroupAndOrigin& aGroupAndOrigin) { + AssertIsOnIOThread(); + + MutexAutoLock lock(mQuotaMutex); + + RefPtr<OriginInfo> originInfo = + LockedGetOriginInfo(PERSISTENCE_TYPE_DEFAULT, aGroupAndOrigin); + if (originInfo && !originInfo->LockedPersisted()) { + originInfo->LockedPersist(); + } +} + +void QuotaManager::AbortOperationsForProcess(ContentParentId aContentParentId) { + AssertIsOnOwningThread(); + + for (const RefPtr<Client>& client : *mClients) { + client->AbortOperationsForProcess(aContentParentId); + } +} + +Result<nsCOMPtr<nsIFile>, nsresult> QuotaManager::GetDirectoryForOrigin( + PersistenceType aPersistenceType, const nsACString& aASCIIOrigin) const { + QM_TRY_UNWRAP(auto directory, + QM_NewLocalFile(GetStoragePath(aPersistenceType))); + + QM_TRY(directory->Append(MakeSanitizedOriginString(aASCIIOrigin))); + + return directory; +} + +nsresult QuotaManager::RestoreDirectoryMetadata2(nsIFile* aDirectory, + bool aPersistent) { + AssertIsOnIOThread(); + MOZ_ASSERT(aDirectory); + MOZ_ASSERT(mStorageConnection); + + RefPtr<RestoreDirectoryMetadata2Helper> helper = + new RestoreDirectoryMetadata2Helper(aDirectory, aPersistent); + + QM_TRY(helper->RestoreMetadata2File()); + + return NS_OK; +} + +Result<QuotaManager::GetDirectoryResultWithQuotaInfo, nsresult> +QuotaManager::GetDirectoryMetadataWithQuotaInfo2(nsIFile* aDirectory) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(aDirectory); + MOZ_ASSERT(mStorageConnection); + + QM_TRY_INSPECT(const auto& binaryStream, + GetBinaryInputStream(*aDirectory, + nsLiteralString(METADATA_V2_FILE_NAME))); + + QM_TRY_INSPECT(const uint64_t& timestamp, + MOZ_TO_RESULT_INVOKE(binaryStream, Read64)); + + QM_TRY_INSPECT(const bool& persisted, + MOZ_TO_RESULT_INVOKE(binaryStream, ReadBoolean)); + + QM_TRY_INSPECT(const bool& reservedData1, + MOZ_TO_RESULT_INVOKE(binaryStream, Read32)); + Unused << reservedData1; + + QM_TRY_INSPECT(const bool& reservedData2, + MOZ_TO_RESULT_INVOKE(binaryStream, Read32)); + Unused << reservedData2; + + QuotaInfo quotaInfo; + + QM_TRY_UNWRAP(quotaInfo.mSuffix, MOZ_TO_RESULT_INVOKE_TYPED( + nsCString, binaryStream, ReadCString)); + + QM_TRY_UNWRAP(quotaInfo.mGroup, MOZ_TO_RESULT_INVOKE_TYPED( + nsCString, binaryStream, ReadCString)); + + QM_TRY_UNWRAP(quotaInfo.mOrigin, MOZ_TO_RESULT_INVOKE_TYPED( + nsCString, binaryStream, ReadCString)); + + // Currently unused (used to be isApp). + QM_TRY_INSPECT(const bool& dummy, + MOZ_TO_RESULT_INVOKE(binaryStream, ReadBoolean)); + Unused << dummy; + + QM_TRY(binaryStream->Close()); + + QM_TRY_INSPECT(const bool& updated, MaybeUpdateGroupForOrigin(quotaInfo)); + + if (updated) { + // Only overwriting .metadata-v2 (used to overwrite .metadata too) to reduce + // I/O. + QM_TRY( + CreateDirectoryMetadata2(*aDirectory, timestamp, persisted, quotaInfo)); + } + + return GetDirectoryResultWithQuotaInfo{ + {static_cast<int64_t>(timestamp), persisted}, std::move(quotaInfo)}; +} + +Result<QuotaManager::GetDirectoryResultWithQuotaInfo, nsresult> +QuotaManager::GetDirectoryMetadataWithQuotaInfo2WithRestore(nsIFile* aDirectory, + bool aPersistent) { + QM_TRY_UNWRAP( + auto maybeFirstAttemptResult, + ([&]() -> Result<Maybe<GetDirectoryResultWithQuotaInfo>, nsresult> { + QM_TRY_RETURN(GetDirectoryMetadataWithQuotaInfo2(aDirectory) + .map(Some<GetDirectoryResultWithQuotaInfo, + GetDirectoryResultWithQuotaInfo>), + Maybe<GetDirectoryResultWithQuotaInfo>{}); + }())); + + if (!maybeFirstAttemptResult) { + QM_TRY(RestoreDirectoryMetadata2(aDirectory, aPersistent)); + + QM_TRY_RETURN(GetDirectoryMetadataWithQuotaInfo2(aDirectory)); + } + + return maybeFirstAttemptResult.extract(); +} + +Result<QuotaManager::GetDirectoryResult, nsresult> +QuotaManager::GetDirectoryMetadata2(nsIFile* aDirectory) { + AssertIsOnIOThread(); + MOZ_ASSERT(aDirectory); + MOZ_ASSERT(mStorageConnection); + + QM_TRY_INSPECT(const auto& binaryStream, + GetBinaryInputStream(*aDirectory, + nsLiteralString(METADATA_V2_FILE_NAME))); + + QM_TRY_INSPECT(const uint64_t& timestamp, + MOZ_TO_RESULT_INVOKE(binaryStream, Read64)); + + QM_TRY_INSPECT(const bool& persisted, + MOZ_TO_RESULT_INVOKE(binaryStream, ReadBoolean)); + + return GetDirectoryResult{static_cast<int64_t>(timestamp), persisted}; +} + +Result<QuotaManager::GetDirectoryResult, nsresult> +QuotaManager::GetDirectoryMetadata2WithRestore(nsIFile* aDirectory, + bool aPersistent) { + QM_TRY_UNWRAP( + auto maybeFirstAttemptResult, + ([this, &aDirectory]() -> Result<Maybe<GetDirectoryResult>, nsresult> { + QM_TRY_RETURN(GetDirectoryMetadata2(aDirectory) + .map(Some<GetDirectoryResult, GetDirectoryResult>), + Maybe<GetDirectoryResult>{}); + }())); + + if (!maybeFirstAttemptResult) { + QM_TRY(RestoreDirectoryMetadata2(aDirectory, aPersistent)); + + QM_TRY_RETURN(GetDirectoryMetadata2(aDirectory)); + } + + return maybeFirstAttemptResult.extract(); +} + +nsresult QuotaManager::InitializeRepository(PersistenceType aPersistenceType) { + 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; + GroupAndOrigin mGroupAndOrigin; + 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]() -> Result<Ok, nsresult> { + QM_TRY_INSPECT( + const bool& isDirectory, + MOZ_TO_RESULT_INVOKE(childDirectory, IsDirectory)); + + QM_TRY_INSPECT( + const auto& leafName, + MOZ_TO_RESULT_INVOKE_TYPED(nsAutoString, childDirectory, + GetLeafName)); + + if (!isDirectory) { + if (IsOSMetadata(leafName) || IsDotFile(leafName)) { + return Ok{}; + } + + // Unknown files during initialization are now allowed. + // Just warn if we find them. + UNKNOWN_FILE_WARNING(leafName); + return Ok{}; + } + + QM_TRY_UNWRAP(auto metadata, + GetDirectoryMetadataWithQuotaInfo2WithRestore( + childDirectory, + /* aPersistent */ false)); + + // 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.mQuotaInfo.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)) { + renameAndInitInfos.AppendElement(RenameAndInitInfo{ + std::move(childDirectory), + std::move(metadata.mQuotaInfo), metadata.mTimestamp, + metadata.mPersisted}); + return Ok{}; + } + + // 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( + InitializeOrigin(aPersistenceType, metadata.mQuotaInfo, + metadata.mTimestamp, + metadata.mPersisted, childDirectory)); + + return Ok{}; + }()), + OK_IN_NIGHTLY_PROPAGATE_IN_OTHERS, statusKeeperFunc); + + return Ok{}; + }), + OK_IN_NIGHTLY_PROPAGATE_IN_OTHERS, statusKeeperFunc); + + return Ok{}; + }())); + + for (const auto& info : renameAndInitInfos) { + QM_TRY(([&]() -> Result<Ok, nsresult> { + QM_TRY(([&directory, &info, this, + aPersistenceType]() -> Result<Ok, nsresult> { + const auto originDirName = + MakeSanitizedOriginString(info.mGroupAndOrigin.mOrigin); + + // Check if targetDirectory exist. + QM_TRY_INSPECT(const auto& targetDirectory, + CloneFileAndAppend(*directory, originDirName)); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE(targetDirectory, Exists)); + + if (exists) { + QM_TRY(info.mOriginDirectory->Remove(true)); + + return Ok{}; + } + + QM_TRY(info.mOriginDirectory->RenameTo(nullptr, originDirName)); + + QM_TRY(InitializeOrigin(aPersistenceType, info.mGroupAndOrigin, + info.mTimestamp, info.mPersisted, + targetDirectory)); + + 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 GroupAndOrigin& aGroupAndOrigin, + 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, &aGroupAndOrigin, + &clientUsages]() -> Result<Ok, nsresult> { + QM_TRY_INSPECT(const bool& isDirectory, + MOZ_TO_RESULT_INVOKE(file, IsDirectory)); + + QM_TRY_INSPECT(const auto& leafName, + MOZ_TO_RESULT_INVOKE_TYPED( + nsAutoString, file, GetLeafName)); + + if (!isDirectory) { + if (IsOriginMetadata(leafName)) { + return Ok{}; + } + + if (IsTempMetadata(leafName)) { + QM_TRY(file->Remove(/* recursive */ false)); + + return Ok{}; + } + + if (IsOSMetadata(leafName) || IsDotFile(leafName)) { + return Ok{}; + } + + // 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. + return Ok{}; + } + + 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); + return Ok{}; + } + + if (trackQuota) { + QM_TRY_INSPECT(const auto& usageInfo, + (*mClients)[clientType]->InitOrigin( + aPersistenceType, aGroupAndOrigin, + /* 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( + aGroupAndOrigin.mGroup) + + u", origin "_ns + + NS_ConvertUTF8toUTF16( + aGroupAndOrigin.mOrigin)) + .get()); + } +#endif + } + } + } else { + QM_TRY((*mClients)[clientType]->InitOriginWithoutTracking( + aPersistenceType, aGroupAndOrigin, + /* aCanceled */ Atomic<bool>(false))); + } + + 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(aPersistenceType, aGroupAndOrigin, clientUsages, + usage.value(), aAccessTime, aPersisted); + } + + return NS_OK; +} + +nsresult +QuotaManager::UpgradeFromIndexedDBDirectoryToPersistentStorageDirectory( + nsIFile* aIndexedDBDir) { + AssertIsOnIOThread(); + MOZ_ASSERT(aIndexedDBDir); + + auto rv = [this, &aIndexedDBDir]() -> nsresult { + bool isDirectory; + QM_TRY(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(persistentStorageDir->Append( + nsLiteralString(PERSISTENT_DIRECTORY_NAME))); + + bool exists; + QM_TRY(persistentStorageDir->Exists(&exists)); + + if (exists) { + QM_WARNING("Deleting old <profile>/indexedDB directory!"); + + QM_TRY(aIndexedDBDir->Remove(/* aRecursive */ true)); + + return NS_OK; + } + + nsCOMPtr<nsIFile> storageDir; + QM_TRY(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(aIndexedDBDir->MoveTo(storageDir, + nsLiteralString(PERSISTENT_DIRECTORY_NAME))); + + return NS_OK; + }(); + + mInitializationInfo.RecordFirstInitializationAttempt( + Initialization::UpgradeFromIndexedDBDirectory, rv); + + return rv; +} + +nsresult +QuotaManager::UpgradeFromPersistentStorageDirectoryToDefaultStorageDirectory( + nsIFile* aPersistentStorageDir) { + AssertIsOnIOThread(); + MOZ_ASSERT(aPersistentStorageDir); + + auto rv = [this, &aPersistentStorageDir]() -> nsresult { + QM_TRY_INSPECT(const bool& isDirectory, + MOZ_TO_RESULT_INVOKE(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(defaultStorageDir, Exists)); + + if (exists) { + QM_WARNING("Deleting old <profile>/storage/persistent directory!"); + + QM_TRY(aPersistentStorageDir->Remove(/* aRecursive */ true)); + + return NS_OK; + } + } + + { + // Create real metadata files for origin directories in persistent + // storage. + auto helper = MakeRefPtr<CreateOrUpgradeDirectoryMetadataHelper>( + aPersistentStorageDir, + /* aPersistent */ true); + + QM_TRY(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(temporaryStorageDir, Exists)); + + if (exists) { + QM_TRY_INSPECT(const bool& isDirectory, + MOZ_TO_RESULT_INVOKE(temporaryStorageDir, IsDirectory)); + + if (!isDirectory) { + NS_WARNING("temporary entry is not a directory!"); + return NS_OK; + } + + helper = MakeRefPtr<CreateOrUpgradeDirectoryMetadataHelper>( + temporaryStorageDir, + /* aPersistent */ false); + + QM_TRY(helper->ProcessRepository()); + } + } + + // And finally rename persistent to default. + QM_TRY(aPersistentStorageDir->RenameTo( + nullptr, nsLiteralString(DEFAULT_DIRECTORY_NAME))); + + return NS_OK; + }(); + + mInitializationInfo.RecordFirstInitializationAttempt( + Initialization::UpgradeFromPersistentStorageDirectory, rv); + + return rv; +} + +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(directory, Exists)); + + if (!exists) { + continue; + } + + bool persistent = persistenceType == PERSISTENCE_TYPE_PERSISTENT; + RefPtr<RepositoryOperationBase> helper = new Helper(directory, persistent); + QM_TRY(helper->ProcessRepository()); + } + +#ifdef DEBUG + { + QM_TRY_INSPECT(const int32_t& storageVersion, + MOZ_TO_RESULT_INVOKE(aConnection, GetSchemaVersion)); + + MOZ_ASSERT(storageVersion == aOldVersion); + } +#endif + + QM_TRY(aConnection->SetSchemaVersion(aNewVersion)); + + return NS_OK; +} + +nsresult QuotaManager::UpgradeStorageFrom0_0To1_0( + mozIStorageConnection* aConnection) { + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + auto rv = [this, &aConnection]() -> nsresult { + QM_TRY(UpgradeStorage<UpgradeStorageFrom0_0To1_0Helper>( + 0, MakeStorageVersion(1, 0), aConnection)); + + return NS_OK; + }(); + + mInitializationInfo.RecordFirstInitializationAttempt( + Initialization::UpgradeStorageFrom0_0To1_0, rv); + + return rv; +} + +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. + + auto rv = [this, &aConnection]() -> nsresult { + QM_TRY(UpgradeStorage<UpgradeStorageFrom1_0To2_0Helper>( + MakeStorageVersion(1, 0), MakeStorageVersion(2, 0), aConnection)); + + return NS_OK; + }(); + + mInitializationInfo.RecordFirstInitializationAttempt( + Initialization::UpgradeStorageFrom1_0To2_0, rv); + + return rv; +} + +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. + + auto rv = [this, &aConnection]() -> nsresult { + QM_TRY(UpgradeStorage<UpgradeStorageFrom2_0To2_1Helper>( + MakeStorageVersion(2, 0), MakeStorageVersion(2, 1), aConnection)); + + return NS_OK; + }(); + + mInitializationInfo.RecordFirstInitializationAttempt( + Initialization::UpgradeStorageFrom2_0To2_1, rv); + + return rv; +} + +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. + + auto rv = [this, &aConnection]() -> nsresult { + QM_TRY(UpgradeStorage<UpgradeStorageFrom2_1To2_2Helper>( + MakeStorageVersion(2, 1), MakeStorageVersion(2, 2), aConnection)); + + return NS_OK; + }(); + + mInitializationInfo.RecordFirstInitializationAttempt( + Initialization::UpgradeStorageFrom2_1To2_2, rv); + + return rv; +} + +nsresult QuotaManager::UpgradeStorageFrom2_2To2_3( + mozIStorageConnection* aConnection) { + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + auto rv = [&aConnection]() -> nsresult { + // Table `database` + QM_TRY(aConnection->ExecuteSimpleSQL( + nsLiteralCString("CREATE TABLE database" + "( cache_version INTEGER NOT NULL DEFAULT 0" + ");"))); + + QM_TRY(aConnection->ExecuteSimpleSQL( + nsLiteralCString("INSERT INTO database (cache_version) " + "VALUES (0)"))); + +#ifdef DEBUG + { + QM_TRY_INSPECT(const int32_t& storageVersion, + MOZ_TO_RESULT_INVOKE(aConnection, GetSchemaVersion)); + + MOZ_ASSERT(storageVersion == MakeStorageVersion(2, 2)); + } +#endif + + QM_TRY(aConnection->SetSchemaVersion(MakeStorageVersion(2, 3))); + + return NS_OK; + }(); + + mInitializationInfo.RecordFirstInitializationAttempt( + Initialization::UpgradeStorageFrom2_2To2_3, rv); + + return rv; +} + +nsresult QuotaManager::MaybeRemoveLocalStorageData() { + AssertIsOnIOThread(); + MOZ_ASSERT(!CachedNextGenLocalStorageEnabled()); + + // Cleanup the tmp file first, if there's any. + { + QM_TRY_INSPECT(const auto& lsArchiveTmpFile, + GetLocalStorageArchiveTmpFile(*mStoragePath)); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE(lsArchiveTmpFile, Exists)); + + if (exists) { + QM_TRY(lsArchiveTmpFile->Remove(false)); + } + } + + // Now check the real archive file. + QM_TRY_INSPECT(const auto& lsArchiveFile, + GetLocalStorageArchiveFile(*mStoragePath)); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE(lsArchiveFile, Exists)); + + if (!exists) { + // If the ls archive doesn't exist then ls directories can't exist either. + return NS_OK; + } + + QM_TRY(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(lsArchiveFile->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(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(originDir, Exists)); + MOZ_ASSERT(exists); + } +#endif + + QM_TRY_INSPECT(const bool& isDirectory, + MOZ_TO_RESULT_INVOKE(originDir, IsDirectory)); + + if (!isDirectory) { + QM_TRY_INSPECT( + const auto& leafName, + MOZ_TO_RESULT_INVOKE_TYPED(nsAutoString, originDir, GetLeafName)); + + // Unknown files during upgrade are allowed. Just warn if we find + // them. + if (!IsOSMetadata(leafName)) { + UNKNOWN_FILE_WARNING(leafName); + } + + return Ok{}; + } + + 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(lsDir, Exists)); + + if (!exists) { + return Ok{}; + } + } + + { + QM_TRY_INSPECT(const bool& isDirectory, + MOZ_TO_RESULT_INVOKE(lsDir, IsDirectory)); + + if (!isDirectory) { + QM_WARNING("ls entry is not a directory!"); + + return Ok{}; + } + } + + nsString path; + QM_TRY(lsDir->GetPath(path)); + + QM_WARNING("Deleting %s directory!", NS_ConvertUTF16toUTF8(path).get()); + + QM_TRY(lsDir->Remove(/* aRecursive */ true)); + + return Ok{}; + })); + + return NS_OK; +} + +Result<nsCOMPtr<mozIStorageConnection>, nsresult> +QuotaManager::CreateLocalStorageArchiveConnectionFromWebAppsStore() { + AssertIsOnIOThread(); + MOZ_ASSERT(CachedNextGenLocalStorageEnabled()); + + QM_TRY_INSPECT(const auto& lsArchiveFile, + GetLocalStorageArchiveFile(*mStoragePath)); + +#ifdef DEBUG + { + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE(lsArchiveFile, Exists)); + MOZ_ASSERT(!exists); + } +#endif + + // Get the storage service first, we will need it at multiple places. + QM_TRY_INSPECT(const auto& ss, ToResultGet<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(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_TYPED(nsAutoCString, *stmt, GetUTF8String, 0)); + + QM_TRY(stmt->Finalize()); + + if (journalMode.EqualsLiteral("wal")) { + // We don't copy the WAL file, so make sure the old database is fully + // checkpointed. + QM_TRY( + connection->ExecuteSimpleSQL("PRAGMA wal_checkpoint(TRUNCATE);"_ns)); + } + + // Explicitely close the connection before the old database is copied. + QM_TRY(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(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_TYPED(nsCOMPtr<mozIStorageConnection>, ss, + OpenUnsharedDatabase, lsArchiveTmpFile)); + + // 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(lsArchiveTmpConnection->ExecuteSimpleSQL( + "PRAGMA journal_mode = DELETE;"_ns)); + + // The connection will be now implicitely closed (it's always safer to + // close database connection before we manipulate underlying file) + } + + // Finally, rename ls-archive-tmp.sqlite to ls-archive.sqlite + QM_TRY(lsArchiveTmpFile->MoveTo(nullptr, + nsLiteralString(LS_ARCHIVE_FILE_NAME))); + + QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr<mozIStorageConnection>, + ss, OpenUnsharedDatabase, + lsArchiveFile)); + } + + // If webappsstore database is not useable, just create an empty archive. + + // Ensure the storage directory actually exists. + QM_TRY_INSPECT(const auto& storageDirectory, + QM_NewLocalFile(GetStoragePath())); + + QM_TRY_INSPECT(const bool& created, EnsureDirectory(*storageDirectory)); + + Unused << created; + + QM_TRY_UNWRAP( + auto lsArchiveConnection, + MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr<mozIStorageConnection>, ss, + OpenUnsharedDatabase, lsArchiveFile)); + + QM_TRY(StorageDBUpdater::Update(lsArchiveConnection)); + + return lsArchiveConnection; +} + +Result<std::pair<nsCOMPtr<mozIStorageConnection>, bool>, nsresult> +QuotaManager::CreateLocalStorageArchiveConnection() { + AssertIsOnIOThread(); + MOZ_ASSERT(CachedNextGenLocalStorageEnabled()); + + { + QM_TRY_INSPECT(const auto& lsArchiveTmpFile, + GetLocalStorageArchiveTmpFile(*mStoragePath)); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE(lsArchiveTmpFile, Exists)); + + if (exists) { + QM_TRY(lsArchiveTmpFile->Remove(false)); + } + } + + // Check if the archive was already successfully created. + QM_TRY_INSPECT(const auto& lsArchiveFile, + GetLocalStorageArchiveFile(*mStoragePath)); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE(lsArchiveFile, Exists)); + + if (exists) { + bool removed = false; + + QM_TRY_INSPECT(const bool& isDirectory, + MOZ_TO_RESULT_INVOKE(lsArchiveFile, IsDirectory)); + + if (isDirectory) { + QM_TRY(lsArchiveFile->Remove(true)); + + removed = true; + } + + QM_TRY_INSPECT(const auto& ss, ToResultGet<nsCOMPtr<mozIStorageService>>( + MOZ_SELECT_OVERLOAD(do_GetService), + MOZ_STORAGE_SERVICE_CONTRACTID)); + + QM_TRY_UNWRAP( + auto connection, + MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr<mozIStorageConnection>, ss, + OpenUnsharedDatabase, lsArchiveFile) + .orElse([&removed, &lsArchiveFile, &ss](const nsresult rv) + -> Result<nsCOMPtr<mozIStorageConnection>, nsresult> { + if (!removed && rv == NS_ERROR_FILE_CORRUPTED) { + QM_TRY(lsArchiveFile->Remove(false)); + + removed = true; + + QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_TYPED( + nsCOMPtr<mozIStorageConnection>, ss, OpenUnsharedDatabase, + lsArchiveFile)); + } + + return Err(rv); + })); + + QM_TRY(ToResult(StorageDBUpdater::Update(connection)) + .orElse([&removed, &connection, &lsArchiveFile, + &ss](const nsresult rv) -> Result<Ok, nsresult> { + if (!removed) { + QM_TRY(connection->Close()); + QM_TRY(lsArchiveFile->Remove(false)); + + removed = true; + + QM_TRY_UNWRAP(connection, + MOZ_TO_RESULT_INVOKE_TYPED( + nsCOMPtr<mozIStorageConnection>, ss, + OpenUnsharedDatabase, lsArchiveFile)); + + QM_TRY(StorageDBUpdater::Update(connection)); + + return Ok{}; + } + + return Err(rv); + })); + + return std::pair{std::move(connection), removed}; + } + + QM_TRY_RETURN(CreateLocalStorageArchiveConnectionFromWebAppsStore().map( + [](auto&& connection) { + return std::pair{std::move(connection), true}; + })); +} + +nsresult QuotaManager::RecreateLocalStorageArchive( + nsCOMPtr<mozIStorageConnection>& aConnection) { + AssertIsOnIOThread(); + MOZ_ASSERT(CachedNextGenLocalStorageEnabled()); + + // Close local storage archive connection. We are going to remove underlying + // file. + QM_TRY(aConnection->Close()); + + QM_TRY(MaybeRemoveLocalStorageDirectories()); + + QM_TRY_INSPECT(const auto& lsArchiveFile, + GetLocalStorageArchiveFile(*mStoragePath)); + +#ifdef DEBUG + { + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE(lsArchiveFile, Exists)); + + MOZ_ASSERT(exists); + } +#endif + + QM_TRY(lsArchiveFile->Remove(false)); + + QM_TRY_UNWRAP(aConnection, + CreateLocalStorageArchiveConnectionFromWebAppsStore()); + + return NS_OK; +} + +nsresult QuotaManager::DowngradeLocalStorageArchive( + nsCOMPtr<mozIStorageConnection>& aConnection) { + AssertIsOnIOThread(); + MOZ_ASSERT(CachedNextGenLocalStorageEnabled()); + + QM_TRY(RecreateLocalStorageArchive(aConnection)); + + QM_TRY( + InitializeLocalStorageArchive(aConnection, kLocalStorageArchiveVersion)); + + return NS_OK; +} + +nsresult QuotaManager::UpgradeLocalStorageArchiveFromLessThan4To4( + nsCOMPtr<mozIStorageConnection>& aConnection) { + AssertIsOnIOThread(); + MOZ_ASSERT(CachedNextGenLocalStorageEnabled()); + + QM_TRY(RecreateLocalStorageArchive(aConnection)); + + QM_TRY(InitializeLocalStorageArchive(aConnection, 4)); + + return NS_OK; +} + +/* +nsresult QuotaManager::UpgradeLocalStorageArchiveFrom4To5( + nsCOMPtr<mozIStorageConnection>& aConnection) { + AssertIsOnIOThread(); + MOZ_ASSERT(CachedNextGenLocalStorageEnabled()); + + nsresult rv = SaveLocalStorageArchiveVersion(aConnection, 5); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} +*/ + +#ifdef DEBUG + +void QuotaManager::AssertStorageIsInitialized() const { + AssertIsOnIOThread(); + MOZ_ASSERT(IsStorageInitialized()); +} + +#endif // DEBUG + +nsresult QuotaManager::EnsureStorageIsInitialized() { + AssertIsOnIOThread(); + + if (mStorageConnection) { + mInitializationInfo.AssertInitializationAttempted(Initialization::Storage); + return NS_OK; + } + + const auto autoRecord = mInitializationInfo.RecordFirstInitializationAttempt( + Initialization::Storage, + [&self = *this] { return static_cast<bool>(self.mStorageConnection); }); + + const auto contextLogExtraInfo = ScopedLogExtraInfo{ + ScopedLogExtraInfo::kTagContext, "Initialization::Storage"_ns}; + + QM_TRY_INSPECT(const auto& storageFile, QM_NewLocalFile(mBasePath)); + + QM_TRY(storageFile->Append(mStorageName + kSQLiteSuffix)); + + QM_TRY_INSPECT(const auto& storageFileExists, + MOZ_TO_RESULT_INVOKE(storageFile, Exists)); + + if (!storageFileExists) { + QM_TRY_INSPECT(const auto& indexedDBDir, QM_NewLocalFile(*mIndexedDBPath)); + + QM_TRY_INSPECT(const auto& indexedDBDirExists, + MOZ_TO_RESULT_INVOKE(indexedDBDir, Exists)); + + if (indexedDBDirExists) { + QM_TRY(UpgradeFromIndexedDBDirectoryToPersistentStorageDirectory( + indexedDBDir)); + } + + QM_TRY_INSPECT(const auto& persistentStorageDir, + QM_NewLocalFile(*mStoragePath)); + + QM_TRY(persistentStorageDir->Append( + nsLiteralString(PERSISTENT_DIRECTORY_NAME))); + + QM_TRY_INSPECT(const auto& persistentStorageDirExists, + MOZ_TO_RESULT_INVOKE(persistentStorageDir, Exists)); + + if (persistentStorageDirExists) { + QM_TRY(UpgradeFromPersistentStorageDirectoryToDefaultStorageDirectory( + persistentStorageDir)); + } + } + + QM_TRY_INSPECT(const auto& ss, ToResultGet<nsCOMPtr<mozIStorageService>>( + MOZ_SELECT_OVERLOAD(do_GetService), + MOZ_STORAGE_SERVICE_CONTRACTID)); + + QM_TRY_UNWRAP(auto connection, + MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr<mozIStorageConnection>, ss, + OpenUnsharedDatabase, storageFile) + .orElse(ErrToOkOrErr<NS_ERROR_FILE_CORRUPTED, nullptr, + nsCOMPtr<mozIStorageConnection>>)); + + if (!connection) { + // Nuke the database file. + QM_TRY(storageFile->Remove(false)); + + QM_TRY_UNWRAP(connection, MOZ_TO_RESULT_INVOKE_TYPED( + nsCOMPtr<mozIStorageConnection>, ss, + OpenUnsharedDatabase, storageFile)); + } + + // We want extra durability for this important file. + QM_TRY(connection->ExecuteSimpleSQL("PRAGMA synchronous = EXTRA;"_ns)); + + // Check to make sure that the storage version is correct. + QM_TRY_UNWRAP(auto storageVersion, + MOZ_TO_RESULT_INVOKE(connection, 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(connection->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(storageDir, Exists)); + + const bool newDirectory = !storageDirExists; + + if (newDatabase) { + // Set the page size first. + if (kSQLitePageSizeOverride) { + QM_TRY(connection->ExecuteSimpleSQL(nsPrintfCString( + "PRAGMA page_size = %" PRIu32 ";", kSQLitePageSizeOverride))); + } + } + + mozStorageTransaction transaction( + connection, false, mozIStorageConnection::TRANSACTION_IMMEDIATE); + + // 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(CreateTables(connection)); + +#ifdef DEBUG + { + QM_TRY_INSPECT(const int32_t& storageVersion, + MOZ_TO_RESULT_INVOKE(connection, GetSchemaVersion), + QM_ASSERT_UNREACHABLE); + MOZ_ASSERT(storageVersion == kStorageVersion); + } +#endif + + QM_TRY(connection->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(UpgradeStorageFrom0_0To1_0(connection)); + } else if (storageVersion == MakeStorageVersion(1, 0)) { + QM_TRY(UpgradeStorageFrom1_0To2_0(connection)); + } else if (storageVersion == MakeStorageVersion(2, 0)) { + QM_TRY(UpgradeStorageFrom2_0To2_1(connection)); + } else if (storageVersion == MakeStorageVersion(2, 1)) { + QM_TRY(UpgradeStorageFrom2_1To2_2(connection)); + } else if (storageVersion == MakeStorageVersion(2, 2)) { + QM_TRY(UpgradeStorageFrom2_2To2_3(connection)); + } 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(connection, GetSchemaVersion)); + } + + MOZ_ASSERT(storageVersion == kStorageVersion); + } + + QM_TRY(transaction.Commit()); + } + + if (CachedNextGenLocalStorageEnabled()) { + QM_TRY_UNWRAP((auto [connection, newlyCreatedOrRecreated]), + CreateLocalStorageArchiveConnection()); + + uint32_t version = 0; + + if (!newlyCreatedOrRecreated) { + QM_TRY_INSPECT(const auto& initialized, + IsLocalStorageArchiveInitialized(*connection)); + + if (initialized) { + QM_TRY_UNWRAP(version, LoadLocalStorageArchiveVersion(*connection)); + } + } + + if (version > kLocalStorageArchiveVersion) { + QM_TRY(DowngradeLocalStorageArchive(connection)); + + QM_TRY_UNWRAP(version, LoadLocalStorageArchiveVersion(*connection)); + + MOZ_ASSERT(version == kLocalStorageArchiveVersion); + } else if (version != kLocalStorageArchiveVersion) { + if (newlyCreatedOrRecreated) { + MOZ_ASSERT(version == 0); + + QM_TRY(InitializeLocalStorageArchive(connection, + kLocalStorageArchiveVersion)); + } else { + static_assert(kLocalStorageArchiveVersion == 4, + "Upgrade function needed due to LocalStorage archive " + "version increase."); + + while (version != kLocalStorageArchiveVersion) { + if (version < 4) { + QM_TRY(UpgradeLocalStorageArchiveFromLessThan4To4(connection)); + } /* else if (version == 4) { + QM_TRY(UpgradeLocalStorageArchiveFrom4To5(connection)); + } */ + else { + QM_FAIL(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); + } + } + } else { + QM_TRY(MaybeRemoveLocalStorageData()); + } + + bool cacheUsable = true; + + QM_TRY_UNWRAP(int32_t cacheVersion, LoadCacheVersion(*connection)); + + if (cacheVersion > kCacheVersion) { + cacheUsable = false; + } else if (cacheVersion != kCacheVersion) { + const bool newCache = !cacheVersion; + + mozStorageTransaction transaction( + connection, false, mozIStorageConnection::TRANSACTION_IMMEDIATE); + + if (newCache) { + QM_TRY(CreateCacheTables(connection)); + +#ifdef DEBUG + { + QM_TRY_INSPECT(const int32_t& cacheVersion, + LoadCacheVersion(*connection)); + MOZ_ASSERT(cacheVersion == kCacheVersion); + } +#endif + + QM_TRY(connection->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_TYPED( + nsCOMPtr<mozIStorageStatement>, + connection, CreateStatement, + "INSERT INTO repository (id, name) " + "VALUES (:id, :name)"_ns)); + } + + QM_TRY(insertStmt->BindInt32ByName("id"_ns, persistenceType)); + + QM_TRY(insertStmt->BindUTF8StringByName( + "name"_ns, PersistenceTypeToString(persistenceType))); + + QM_TRY(insertStmt->Execute()); + } + } else { + // This logic needs to change next time we change the cache! + static_assert(kCacheVersion == 1, + "Upgrade function needed due to cache version increase."); + + while (cacheVersion != kCacheVersion) { + /* if (cacheVersion == 1) { + QM_TRY(UpgradeCacheFrom1To2(connection)); + } else */ + { + QM_FAIL(NS_ERROR_FAILURE, []() { + QM_WARNING( + "Unable to initialize cache, no upgrade path is available!"); + }); + } + + QM_TRY_UNWRAP(cacheVersion, LoadCacheVersion(*connection)); + } + + MOZ_ASSERT(cacheVersion == kCacheVersion); + } + + QM_TRY(transaction.Commit()); + } + + if (cacheUsable && gInvalidateQuotaCache) { + QM_TRY(InvalidateCache(*connection)); + + gInvalidateQuotaCache = false; + } + + mStorageConnection = std::move(connection); + mCacheUsable = cacheUsable; + + return NS_OK; +} + +already_AddRefed<DirectoryLock> QuotaManager::OpenDirectory( + PersistenceType aPersistenceType, const GroupAndOrigin& aGroupAndOrigin, + Client::Type aClientType, bool aExclusive, + RefPtr<OpenDirectoryListener> aOpenListener) { + AssertIsOnOwningThread(); + + bool blocked; + RefPtr<DirectoryLockImpl> lock = CreateDirectoryLock( + Nullable<PersistenceType>(aPersistenceType), aGroupAndOrigin.mGroup, + OriginScope::FromOrigin(aGroupAndOrigin.mOrigin), + Nullable<Client::Type>(aClientType), aExclusive, false, + std::move(aOpenListener), blocked); + MOZ_ASSERT(lock); + + return blocked ? lock.forget() : nullptr; +} + +already_AddRefed<DirectoryLock> QuotaManager::OpenDirectoryInternal( + const Nullable<PersistenceType>& aPersistenceType, + const OriginScope& aOriginScope, const Nullable<Client::Type>& aClientType, + bool aExclusive, OpenDirectoryListener* aOpenListener) { + AssertIsOnOwningThread(); + + bool blocked; + RefPtr<DirectoryLockImpl> lock = + CreateDirectoryLock(aPersistenceType, ""_ns, aOriginScope, + Nullable<Client::Type>(aClientType), aExclusive, true, + aOpenListener, blocked); + MOZ_ASSERT(lock); + + if (!aExclusive) { + return blocked ? lock.forget() : nullptr; + } + + // All the locks that block this new exclusive lock need to be invalidated. + // We also need to notify clients to abort operations for them. + AutoTArray<Client::DirectoryLockIdTable, Client::TYPE_MAX> lockIds; + lockIds.SetLength(Client::TypeMax()); + + const auto& blockedOnLocks = lock->GetBlockedOnLocks(); + + for (DirectoryLockImpl* blockedOnLock : blockedOnLocks) { + if (!blockedOnLock->IsInternal()) { + blockedOnLock->Invalidate(); + + // Clients don't have to handle pending locks. Invalidation is sufficient + // in that case (once a lock is ready and the listener needs to be + // notified, we will call DirectoryLockFailed instead of + // DirectoryLockAcquired which should release any remaining references to + // the lock). + if (!blockedOnLock->IsPending()) { + lockIds[blockedOnLock->ClientType()].Put(blockedOnLock->Id()); + } + } + } + + for (Client::Type type : AllClientTypes()) { + if (lockIds[type].Filled()) { + (*mClients)[type]->AbortOperationsForLocks(lockIds[type]); + } + } + + return blocked ? lock.forget() : nullptr; +} + +Result<std::pair<nsCOMPtr<nsIFile>, bool>, nsresult> +QuotaManager::EnsurePersistentOriginIsInitialized(const QuotaInfo& aQuotaInfo) { + AssertIsOnIOThread(); + MOZ_DIAGNOSTIC_ASSERT(mStorageConnection); + + auto res = [&aQuotaInfo, this]() + -> mozilla::Result<std::pair<nsCOMPtr<nsIFile>, bool>, nsresult> { + QM_TRY_UNWRAP( + auto directory, + GetDirectoryForOrigin(PERSISTENCE_TYPE_PERSISTENT, aQuotaInfo.mOrigin)); + + if (mInitializedOrigins.Contains(aQuotaInfo.mOrigin)) { + 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, + &aQuotaInfo]() -> Result<int64_t, nsresult> { + if (created) { + const int64_t timestamp = PR_Now(); + + // Only creating .metadata-v2 to reduce IO. + QM_TRY(CreateDirectoryMetadata2(*directory, timestamp, + /* aPersisted */ true, aQuotaInfo)); + + return timestamp; + } + + // Get the metadata. We only use the timestamp. + QM_TRY_INSPECT(const auto& metadata, GetDirectoryMetadata2WithRestore( + directory, + /* aPersistent */ true)); + + MOZ_ASSERT(metadata.mTimestamp <= PR_Now()); + + return metadata.mTimestamp; + }())); + + QM_TRY(InitializeOrigin(PERSISTENCE_TYPE_PERSISTENT, aQuotaInfo, timestamp, + /* aPersisted */ true, directory)); + + mInitializedOrigins.AppendElement(aQuotaInfo.mOrigin); + + return std::pair(std::move(directory), created); + }(); + + auto& info = mOriginInitializationInfos.GetOrInsert(aQuotaInfo.mOrigin); + if (!info.mPersistentOriginAttempted) { + Telemetry::Accumulate(Telemetry::QM_FIRST_INITIALIZATION_ATTEMPT, + kPersistentOriginTelemetryKey, + static_cast<uint32_t>(res.isOk())); + + info.mPersistentOriginAttempted = true; + } + + return res; +} + +Result<std::pair<nsCOMPtr<nsIFile>, bool>, nsresult> +QuotaManager::EnsureTemporaryOriginIsInitialized( + PersistenceType aPersistenceType, const QuotaInfo& aQuotaInfo) { + AssertIsOnIOThread(); + MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT); + MOZ_DIAGNOSTIC_ASSERT(mStorageConnection); + MOZ_DIAGNOSTIC_ASSERT(mTemporaryStorageInitialized); + + auto res = [&aPersistenceType, &aQuotaInfo, this]() + -> mozilla::Result<std::pair<nsCOMPtr<nsIFile>, bool>, nsresult> { + // Get directory for this origin and persistence type. + QM_TRY_UNWRAP(auto directory, + GetDirectoryForOrigin(aPersistenceType, aQuotaInfo.mOrigin)); + + QM_TRY_INSPECT(const bool& created, EnsureOriginDirectory(*directory)); + + if (created) { + int64_t timestamp; + NoteOriginDirectoryCreated(aPersistenceType, aQuotaInfo, + /* aPersisted */ false, timestamp); + + // Only creating .metadata-v2 to reduce IO. + QM_TRY(CreateDirectoryMetadata2(*directory, timestamp, + /* aPersisted */ false, aQuotaInfo)); + } + + // TODO: If the metadata file exists and we didn't call + // GetDirectoryMetadata2WithRestore 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 + // GetDirectoryMetadata2WithRestore will be called for the metadata + // file in next session in LoadQuotaFromCache. + + return std::pair(std::move(directory), created); + }(); + + auto& info = mOriginInitializationInfos.GetOrInsert(aQuotaInfo.mOrigin); + if (!info.mTemporaryOriginAttempted) { + Telemetry::Accumulate(Telemetry::QM_FIRST_INITIALIZATION_ATTEMPT, + kTemporaryOriginTelemetryKey, + static_cast<uint32_t>(res.isOk())); + + info.mTemporaryOriginAttempted = true; + } + + return res; +} + +nsresult QuotaManager::EnsureTemporaryStorageIsInitialized() { + AssertIsOnIOThread(); + MOZ_DIAGNOSTIC_ASSERT(mStorageConnection); + + if (mTemporaryStorageInitialized) { + mInitializationInfo.AssertInitializationAttempted( + Initialization::TemporaryStorage); + return NS_OK; + } + + const auto autoRecord = mInitializationInfo.RecordFirstInitializationAttempt( + Initialization::TemporaryStorage, + [&self = *this] { return self.mTemporaryStorageInitialized; }); + + const auto contextLogExtraInfo = ScopedLogExtraInfo{ + ScopedLogExtraInfo::kTagContext, "Initialization::TemporaryStorage"_ns}; + + QM_TRY_INSPECT( + const auto& storageDir, + ToResultGet<nsCOMPtr<nsIFile>>(MOZ_SELECT_OVERLOAD(do_CreateInstance), + NS_LOCAL_FILE_CONTRACTID)); + + QM_TRY(storageDir->InitWithPath(GetStoragePath())); + + // The storage directory must exist before calling GetDiskSpaceAvailable. + QM_TRY_INSPECT(const bool& created, EnsureDirectory(*storageDir)); + + Unused << created; + + // Check for available disk space users have on their device where storage + // directory lives. + QM_TRY_INSPECT(const int64_t& diskSpaceAvailable, + MOZ_TO_RESULT_INVOKE(storageDir, GetDiskSpaceAvailable)); + + MOZ_ASSERT(diskSpaceAvailable >= 0); + + QM_TRY(LoadQuota()); + + mTemporaryStorageInitialized = true; + + // Available disk space shouldn't be used directly for temporary storage + // limit calculation since available disk space is affected by existing data + // stored in temporary storage. So we need to increase it by the temporary + // storage size (that has been calculated in LoadQuota) before passing to + // GetTemporaryStorageLimit.. + mTemporaryStorageLimit = GetTemporaryStorageLimit( + /* aAvailableSpaceBytes */ diskSpaceAvailable + mTemporaryStorageUsage); + + CheckTemporaryStorageLimits(); + + return NS_OK; +} + +void QuotaManager::ShutdownStorage() { + AssertIsOnIOThread(); + + if (mStorageConnection) { + mOriginInitializationInfos.Clear(); + mInitializedOrigins.Clear(); + + if (mTemporaryStorageInitialized) { + if (mCacheUsable) { + UnloadQuota(); + } else { + RemoveQuota(); + } + + mTemporaryStorageInitialized = false; + } + + ReleaseIOThreadObjects(); + + mStorageConnection = nullptr; + mCacheUsable = false; + } + + mInitializationInfo.ResetInitializationAttempts(); +} + +Result<bool, nsresult> QuotaManager::EnsureOriginDirectory( + nsIFile& aDirectory) { + AssertIsOnIOThread(); + + QM_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE(aDirectory, Exists)); + + if (!exists) { + QM_TRY_INSPECT(const auto& leafName, + MOZ_TO_RESULT_INVOKE_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((*mClients)[type]->AboutToClearOrigins(aPersistenceType, + aOriginScope)); + } + } else { + QM_TRY((*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); + } +} + +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 2GB. + const uint64_t x = std::min<uint64_t>(mTemporaryStorageLimit / 5, 2 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)); +} + +uint64_t QuotaManager::GetGroupUsage(const nsACString& aGroup) { + AssertIsOnIOThread(); + + uint64_t usage = 0; + + { + MutexAutoLock lock(mQuotaMutex); + + GroupInfoPair* pair; + if (mGroupInfoPairs.Get(aGroup, &pair)) { + for (const PersistenceType type : kBestEffortPersistenceTypes) { + RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(type); + if (groupInfo) { + AssertNoOverflow(usage, groupInfo->mUsage); + usage += groupInfo->mUsage; + } + } + } + } + + return usage; +} + +uint64_t QuotaManager::GetOriginUsage(const GroupAndOrigin& aGroupAndOrigin) { + AssertIsOnIOThread(); + + uint64_t usage = 0; + + { + MutexAutoLock lock(mQuotaMutex); + + GroupInfoPair* pair; + if (mGroupInfoPairs.Get(aGroupAndOrigin.mGroup, &pair)) { + for (const PersistenceType type : kBestEffortPersistenceTypes) { + RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(type); + if (groupInfo) { + RefPtr<OriginInfo> originInfo = + groupInfo->LockedGetOriginInfo(aGroupAndOrigin.mOrigin); + if (originInfo) { + AssertNoOverflow(usage, originInfo->LockedUsage()); + usage += originInfo->LockedUsage(); + } + } + } + } + } + + return usage; +} + +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. + RefPtr<MozURL> specURL; + nsresult rv = MozURL::Init(getter_AddRefs(specURL), info.spec()); + if (NS_WARN_IF(NS_FAILED(rv))) { + QM_WARNING("A URL %s is not recognized by MozURL", info.spec().get()); + return false; + } + + // Verify the principal originNoSuffix matches spec. + nsCString originNoSuffix; + specURL->Origin(originNoSuffix); + + 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. + nsCString baseDomain; + rv = specURL->BaseDomain(baseDomain); + if (NS_WARN_IF(NS_FAILED(rv))) { + return 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; +} + +// static +QuotaInfo 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(); + + QuotaInfo quotaInfo; + + info.attrs().CreateSuffix(quotaInfo.mSuffix); + + quotaInfo.mGroup = info.baseDomain() + quotaInfo.mSuffix; + + quotaInfo.mOrigin = info.originNoSuffix() + quotaInfo.mSuffix; + + return quotaInfo; + } + + default: { + MOZ_CRASH("Should never get here!"); + } + } +} + +// 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<QuotaInfo, nsresult> QuotaManager::GetInfoFromPrincipal( + nsIPrincipal* aPrincipal) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + + if (aPrincipal->IsSystemPrincipal()) { + return GetInfoForChrome(); + } + + if (aPrincipal->GetIsNullPrincipal()) { + NS_WARNING("IndexedDB not supported from this principal!"); + return Err(NS_ERROR_FAILURE); + } + + QuotaInfo quotaInfo; + + QM_TRY(aPrincipal->GetOrigin(quotaInfo.mOrigin)); + + if (quotaInfo.mOrigin.EqualsLiteral(kChromeOrigin)) { + NS_WARNING("Non-chrome principal can't use chrome origin!"); + return Err(NS_ERROR_FAILURE); + } + + aPrincipal->OriginAttributesRef().CreateSuffix(quotaInfo.mSuffix); + + nsAutoCString baseDomain; + QM_TRY(aPrincipal->GetBaseDomain(baseDomain)); + + MOZ_ASSERT(!baseDomain.IsEmpty()); + + quotaInfo.mGroup = baseDomain + quotaInfo.mSuffix; + + return quotaInfo; +} + +// 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_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 +QuotaInfo QuotaManager::GetInfoForChrome() { + return {{}, GetOriginForChrome(), GetOriginForChrome()}; +} + +// static +nsLiteralCString QuotaManager::GetOriginForChrome() { + return nsLiteralCString{kChromeOrigin}; +} + +// static +bool QuotaManager::IsOriginInternal(const nsACString& aOrigin) { + MOZ_ASSERT(!aOrigin.IsEmpty()); + + // The first prompt is not required for these origins. + if (aOrigin.EqualsLiteral(kChromeOrigin) || + StringBeginsWith(aOrigin, nsDependentCString(kAboutHomeOriginPrefix)) || + StringBeginsWith(aOrigin, nsDependentCString(kIndexedDBOriginPrefix)) || + StringBeginsWith(aOrigin, nsDependentCString(kResourceOriginPrefix))) { + return true; + } + + return false; +} + +// static +bool QuotaManager::AreOriginsEqualOnDisk(const nsACString& aOrigin1, + const nsACString& aOrigin2) { + return MakeSanitizedOriginCString(aOrigin1) == + MakeSanitizedOriginCString(aOrigin2); +} + +// static +Result<PrincipalInfo, nsresult> QuotaManager::ParseOrigin( + const nsACString& aOrigin) { + // An origin string either corresponds to a SystemPrincipalInfo or a + // ContentPrincipalInfo, see + // QuotaManager::GetOriginFromValidatedPrincipalInfo. + + if (aOrigin.Equals(kChromeOrigin)) { + return PrincipalInfo{SystemPrincipalInfo{}}; + } + + ContentPrincipalInfo contentPrincipalInfo; + + nsCString originalSuffix; + const OriginParser::ResultType result = OriginParser::ParseOrigin( + MakeSanitizedOriginCString(aOrigin), contentPrincipalInfo.spec(), + &contentPrincipalInfo.attrs(), originalSuffix); + QM_TRY(OkIf(result == OriginParser::ValidOrigin), Err(NS_ERROR_FAILURE)); + + return PrincipalInfo{std::move(contentPrincipalInfo)}; +} + +// static +void QuotaManager::InvalidateQuotaCache() { gInvalidateQuotaCache = true; } + +uint64_t QuotaManager::LockedCollectOriginsForEviction( + uint64_t aMinSizeToBeFreed, nsTArray<RefPtr<DirectoryLockImpl>>& 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::LockedRemoveQuotaForOrigin( + PersistenceType aPersistenceType, const GroupAndOrigin& aGroupAndOrigin) { + mQuotaMutex.AssertCurrentThreadOwns(); + MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT); + + GroupInfoPair* pair; + if (!mGroupInfoPairs.Get(aGroupAndOrigin.mGroup, &pair)) { + return; + } + + MOZ_ASSERT(pair); + + RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(aPersistenceType); + if (groupInfo) { + groupInfo->LockedRemoveOriginInfo(aGroupAndOrigin.mOrigin); + + if (!groupInfo->LockedHasOriginInfos()) { + pair->LockedClearGroupInfo(aPersistenceType); + + if (!pair->LockedHasGroupInfos()) { + mGroupInfoPairs.Remove(aGroupAndOrigin.mGroup); + } + } + } +} + +already_AddRefed<GroupInfo> QuotaManager::LockedGetOrCreateGroupInfo( + PersistenceType aPersistenceType, const nsACString& aGroup) { + mQuotaMutex.AssertCurrentThreadOwns(); + MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT); + + GroupInfoPair* pair; + if (!mGroupInfoPairs.Get(aGroup, &pair)) { + pair = new GroupInfoPair(); + mGroupInfoPairs.Put(aGroup, pair); + // The hashtable is now responsible to delete the GroupInfoPair. + } + + RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(aPersistenceType); + if (!groupInfo) { + groupInfo = new GroupInfo(pair, aPersistenceType, aGroup); + pair->LockedSetGroupInfo(aPersistenceType, groupInfo); + } + + return groupInfo.forget(); +} + +already_AddRefed<OriginInfo> QuotaManager::LockedGetOriginInfo( + PersistenceType aPersistenceType, const GroupAndOrigin& aGroupAndOrigin) { + mQuotaMutex.AssertCurrentThreadOwns(); + MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT); + + GroupInfoPair* pair; + if (mGroupInfoPairs.Get(aGroupAndOrigin.mGroup, &pair)) { + RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(aPersistenceType); + if (groupInfo) { + return groupInfo->LockedGetOriginInfo(aGroupAndOrigin.mOrigin); + } + } + + return nullptr; +} + +void QuotaManager::CheckTemporaryStorageLimits() { + AssertIsOnIOThread(); + + const auto doomedOrigins = [this] { + const auto doomedOriginInfos = [this] { + nsTArray<NotNull<RefPtr<OriginInfo>>> doomedOriginInfos; + MutexAutoLock lock(mQuotaMutex); + + for (const auto& entry : mGroupInfoPairs) { + const auto& pair = entry.GetData(); + + MOZ_ASSERT(!entry.GetKey().IsEmpty()); + MOZ_ASSERT(pair); + + uint64_t groupUsage = 0; + + RefPtr<GroupInfo> temporaryGroupInfo = + pair->LockedGetGroupInfo(PERSISTENCE_TYPE_TEMPORARY); + if (temporaryGroupInfo) { + groupUsage += temporaryGroupInfo->mUsage; + } + + RefPtr<GroupInfo> defaultGroupInfo = + pair->LockedGetGroupInfo(PERSISTENCE_TYPE_DEFAULT); + if (defaultGroupInfo) { + groupUsage += defaultGroupInfo->mUsage; + } + + if (groupUsage > 0) { + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager, "Shouldn't be null!"); + + if (groupUsage > quotaManager->GetGroupLimit()) { + nsTArray<NotNull<RefPtr<OriginInfo>>> originInfos; + if (temporaryGroupInfo) { + originInfos.AppendElements(temporaryGroupInfo->mOriginInfos); + } + if (defaultGroupInfo) { + originInfos.AppendElements(defaultGroupInfo->mOriginInfos); + } + originInfos.Sort(OriginInfoLRUComparator()); + + for (uint32_t i = 0; i < originInfos.Length(); i++) { + const NotNull<RefPtr<OriginInfo>>& originInfo = originInfos[i]; + if (originInfo->LockedPersisted()) { + continue; + } + + doomedOriginInfos.AppendElement(originInfo); + groupUsage -= originInfo->LockedUsage(); + + if (groupUsage <= quotaManager->GetGroupLimit()) { + break; + } + } + } + } + } + + uint64_t usage = std::accumulate( + doomedOriginInfos.cbegin(), doomedOriginInfos.cend(), uint64_t(0), + [](uint64_t oldValue, const auto& originInfo) { + return oldValue + originInfo->LockedUsage(); + }); + + if (mTemporaryStorageUsage - usage > mTemporaryStorageLimit) { + nsTArray<NotNull<RefPtr<OriginInfo>>> originInfos; + + 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) { + originInfos.AppendElements(groupInfo->mOriginInfos); + } + + groupInfo = pair->LockedGetGroupInfo(PERSISTENCE_TYPE_DEFAULT); + if (groupInfo) { + originInfos.AppendElements(groupInfo->mOriginInfos); + } + } + + originInfos.RemoveElementsBy( + [&doomedOriginInfos](const auto& originInfo) { + return doomedOriginInfos.Contains(originInfo) || + originInfo->LockedPersisted(); + }); + + originInfos.Sort(OriginInfoLRUComparator()); + + for (uint32_t i = 0; i < originInfos.Length(); i++) { + if (mTemporaryStorageUsage - usage <= mTemporaryStorageLimit) { + originInfos.TruncateLength(i); + break; + } + + usage += originInfos[i]->LockedUsage(); + } + + doomedOriginInfos.AppendElements(originInfos); + } + + return doomedOriginInfos; + }(); + + for (const auto& doomedOriginInfo : doomedOriginInfos) { +#ifdef DEBUG + { + MutexAutoLock lock(mQuotaMutex); + MOZ_ASSERT(!doomedOriginInfo->LockedPersisted()); + } +#endif + + DeleteFilesForOrigin(doomedOriginInfo->mGroupInfo->mPersistenceType, + doomedOriginInfo->mOrigin); + } + + nsTArray<OriginParams> doomedOrigins; + { + MutexAutoLock lock(mQuotaMutex); + + for (const auto& doomedOriginInfo : doomedOriginInfos) { + PersistenceType persistenceType = + doomedOriginInfo->mGroupInfo->mPersistenceType; + const GroupAndOrigin groupAndOrigin = { + doomedOriginInfo->mGroupInfo->mGroup, doomedOriginInfo->mOrigin}; + LockedRemoveQuotaForOrigin(persistenceType, groupAndOrigin); + + doomedOrigins.EmplaceBack( + OriginParams(persistenceType, groupAndOrigin.mOrigin)); + } + } + + return doomedOrigins; + }(); + + for (const OriginParams& doomedOrigin : doomedOrigins) { + OriginClearCompleted(doomedOrigin.mPersistenceType, doomedOrigin.mOrigin, + Nullable<Client::Type>()); + } + + if (mTemporaryStorageUsage > mTemporaryStorageLimit) { + // If disk space is still low after origin clear, notify storage pressure. + NotifyStoragePressure(mTemporaryStorageUsage); + } +} + +void QuotaManager::DeleteFilesForOrigin(PersistenceType aPersistenceType, + const nsACString& aOrigin) { + QM_TRY_INSPECT(const auto& directory, + GetDirectoryForOrigin(aPersistenceType, aOrigin), QM_VOID); + + nsresult rv = directory->Remove(true); + if (rv != NS_ERROR_FILE_TARGET_DOES_NOT_EXIST && + 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<DirectoryLockImpl>>&& aLocks) { + NS_ASSERTION(!NS_IsMainThread(), "Wrong thread!"); + + RefPtr<FinalizeOriginEvictionOp> op = + new FinalizeOriginEvictionOp(mOwningThread, std::move(aLocks)); + + if (IsOnIOThread()) { + op->RunOnIOThreadImmediately(); + } else { + op->Dispatch(); + } +} + +auto QuotaManager::GetDirectoryLockTable(PersistenceType aPersistenceType) + -> DirectoryLockTable& { + switch (aPersistenceType) { + case PERSISTENCE_TYPE_TEMPORARY: + return mTemporaryDirectoryLockTable; + case PERSISTENCE_TYPE_DEFAULT: + return mDefaultDirectoryLockTable; + + case PERSISTENCE_TYPE_PERSISTENT: + case PERSISTENCE_TYPE_INVALID: + default: + MOZ_CRASH("Bad persistence type value!"); + } +} + +bool QuotaManager::IsSanitizedOriginValid(const nsACString& aSanitizedOrigin) { + AssertIsOnIOThread(); + + bool valid; + if (auto entry = mValidOrigins.LookupForAdd(aSanitizedOrigin)) { + // We already parsed this sanitized origin string. + valid = entry.Data(); + } else { + nsCString spec; + OriginAttributes attrs; + nsCString originalSuffix; + OriginParser::ResultType result = OriginParser::ParseOrigin( + aSanitizedOrigin, spec, &attrs, originalSuffix); + + valid = result == OriginParser::ValidOrigin; + entry.OrInsert([valid]() { return valid; }); + } + + return valid; +} + +int64_t QuotaManager::GenerateDirectoryLockId() { + const int64_t directorylockId = mNextDirectoryLockId; + + CheckedInt64 result = CheckedInt64(mNextDirectoryLockId) + 1; + if (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; +} + +/******************************************************************************* + * Local class implementations + ******************************************************************************/ + +void ClientUsageArray::Serialize(nsACString& aText) const { + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + bool first = true; + + for (Client::Type type : quotaManager->AllClientTypes()) { + const Maybe<uint64_t>& clientUsage = ElementAt(type); + if (clientUsage.isSome()) { + if (first) { + first = false; + } else { + aText.Append(" "); + } + + aText.Append(Client::TypeToPrefix(type)); + aText.AppendInt(clientUsage.value()); + } + } +} + +nsresult ClientUsageArray::Deserialize(const nsACString& aText) { + for (const auto& token : + nsCCharSeparatedTokenizerTemplate<NS_TokenizerIgnoreNothing>(aText, ' ') + .ToRange()) { + QM_TRY(OkIf(token.Length() >= 2), NS_ERROR_FAILURE); + + Client::Type clientType; + QM_TRY(OkIf(Client::TypeFromPrefix(token.First(), clientType, fallible)), + NS_ERROR_FAILURE); + + nsresult rv; + const uint64_t usage = Substring(token, 1).ToInteger(&rv); + QM_TRY(ToResult(rv)); + + ElementAt(clientType) = Some(usage); + } + + return NS_OK; +} + +OriginInfo::OriginInfo(GroupInfo* aGroupInfo, const nsACString& aOrigin, + const ClientUsageArray& aClientUsages, uint64_t aUsage, + int64_t aAccessTime, bool aPersisted, + bool aDirectoryExists) + : mClientUsages(aClientUsages.Clone()), + mGroupInfo(aGroupInfo), + mOrigin(aOrigin), + mUsage(aUsage), + mAccessTime(aAccessTime), + mAccessed(false), + mPersisted(aPersisted), + mDirectoryExists(aDirectoryExists) { + MOZ_ASSERT(aGroupInfo); + MOZ_ASSERT(aClientUsages.Length() == Client::TypeMax()); + MOZ_ASSERT_IF(aPersisted, + aGroupInfo->mPersistenceType == PERSISTENCE_TYPE_DEFAULT); + +#ifdef DEBUG + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + uint64_t usage = 0; + for (Client::Type type : quotaManager->AllClientTypes()) { + AssertNoOverflow(usage, aClientUsages[type].valueOr(0)); + usage += aClientUsages[type].valueOr(0); + } + MOZ_ASSERT(aUsage == usage); +#endif + + MOZ_COUNT_CTOR(OriginInfo); +} + +nsresult OriginInfo::LockedBindToStatement( + mozIStorageStatement* aStatement) const { + AssertCurrentThreadOwnsQuotaMutex(); + MOZ_ASSERT(mGroupInfo); + + QM_TRY(aStatement->BindInt32ByName("repository_id"_ns, + mGroupInfo->mPersistenceType)); + + QM_TRY(aStatement->BindUTF8StringByName("origin"_ns, mOrigin)); + QM_TRY(aStatement->BindUTF8StringByName("group_"_ns, mGroupInfo->mGroup)); + + nsCString clientUsagesText; + mClientUsages.Serialize(clientUsagesText); + + QM_TRY( + aStatement->BindUTF8StringByName("client_usages"_ns, clientUsagesText)); + QM_TRY(aStatement->BindInt64ByName("usage"_ns, mUsage)); + QM_TRY(aStatement->BindInt64ByName("last_access_time"_ns, mAccessTime)); + QM_TRY(aStatement->BindInt32ByName("accessed"_ns, mAccessed)); + QM_TRY(aStatement->BindInt32ByName("persisted"_ns, mPersisted)); + + return NS_OK; +} + +void OriginInfo::LockedDecreaseUsage(Client::Type aClientType, int64_t aSize) { + AssertCurrentThreadOwnsQuotaMutex(); + + MOZ_ASSERT(mClientUsages[aClientType].isSome()); + AssertNoUnderflow(mClientUsages[aClientType].value(), aSize); + mClientUsages[aClientType] = Some(mClientUsages[aClientType].value() - aSize); + + AssertNoUnderflow(mUsage, aSize); + mUsage -= aSize; + + if (!LockedPersisted()) { + AssertNoUnderflow(mGroupInfo->mUsage, aSize); + mGroupInfo->mUsage -= aSize; + } + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + AssertNoUnderflow(quotaManager->mTemporaryStorageUsage, aSize); + quotaManager->mTemporaryStorageUsage -= aSize; +} + +void OriginInfo::LockedResetUsageForClient(Client::Type aClientType) { + AssertCurrentThreadOwnsQuotaMutex(); + + uint64_t size = mClientUsages[aClientType].valueOr(0); + + mClientUsages[aClientType].reset(); + + AssertNoUnderflow(mUsage, size); + mUsage -= size; + + if (!LockedPersisted()) { + AssertNoUnderflow(mGroupInfo->mUsage, size); + mGroupInfo->mUsage -= size; + } + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + AssertNoUnderflow(quotaManager->mTemporaryStorageUsage, size); + quotaManager->mTemporaryStorageUsage -= size; +} + +UsageInfo OriginInfo::LockedGetUsageForClient(Client::Type aClientType) { + AssertCurrentThreadOwnsQuotaMutex(); + + // The current implementation of this method only supports DOMCACHE and LS, + // which only use DatabaseUsage. If this assertion is lifted, the logic below + // must be adapted. + MOZ_ASSERT(aClientType == Client::Type::DOMCACHE || + aClientType == Client::Type::LS); + + return UsageInfo{DatabaseUsageType{mClientUsages[aClientType]}}; +} + +void OriginInfo::LockedPersist() { + AssertCurrentThreadOwnsQuotaMutex(); + MOZ_ASSERT(mGroupInfo->mPersistenceType == PERSISTENCE_TYPE_DEFAULT); + MOZ_ASSERT(!mPersisted); + + mPersisted = true; + + // Remove Usage from GroupInfo + AssertNoUnderflow(mGroupInfo->mUsage, mUsage); + mGroupInfo->mUsage -= mUsage; +} + +already_AddRefed<OriginInfo> GroupInfo::LockedGetOriginInfo( + const nsACString& aOrigin) { + AssertCurrentThreadOwnsQuotaMutex(); + + for (const auto& originInfo : mOriginInfos) { + if (originInfo->mOrigin == aOrigin) { + RefPtr<OriginInfo> result = originInfo; + return result.forget(); + } + } + + return nullptr; +} + +void GroupInfo::LockedAddOriginInfo(NotNull<RefPtr<OriginInfo>>&& aOriginInfo) { + AssertCurrentThreadOwnsQuotaMutex(); + + NS_ASSERTION(!mOriginInfos.Contains(aOriginInfo), + "Replacing an existing entry!"); + mOriginInfos.AppendElement(std::move(aOriginInfo)); + + uint64_t usage = aOriginInfo->LockedUsage(); + + if (!aOriginInfo->LockedPersisted()) { + AssertNoOverflow(mUsage, usage); + mUsage += usage; + } + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + AssertNoOverflow(quotaManager->mTemporaryStorageUsage, usage); + quotaManager->mTemporaryStorageUsage += usage; +} + +void GroupInfo::LockedAdjustUsageForRemovedOriginInfo( + const OriginInfo& aOriginInfo) { + const uint64_t usage = aOriginInfo.LockedUsage(); + + if (!aOriginInfo.LockedPersisted()) { + AssertNoUnderflow(mUsage, usage); + mUsage -= usage; + } + + QuotaManager* const quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + AssertNoUnderflow(quotaManager->mTemporaryStorageUsage, usage); + quotaManager->mTemporaryStorageUsage -= usage; +} + +void GroupInfo::LockedRemoveOriginInfo(const nsACString& aOrigin) { + AssertCurrentThreadOwnsQuotaMutex(); + + const auto foundIt = std::find_if(mOriginInfos.cbegin(), mOriginInfos.cend(), + [&aOrigin](const auto& originInfo) { + return originInfo->mOrigin == aOrigin; + }); + + // XXX Or can we MOZ_ASSERT(foundIt != mOriginInfos.cend()) ? + if (foundIt != mOriginInfos.cend()) { + LockedAdjustUsageForRemovedOriginInfo(**foundIt); + + mOriginInfos.RemoveElementAt(foundIt); + } +} + +void GroupInfo::LockedRemoveOriginInfos() { + AssertCurrentThreadOwnsQuotaMutex(); + + for (const auto& originInfo : std::exchange(mOriginInfos, {})) { + LockedAdjustUsageForRemovedOriginInfo(*originInfo); + } +} + +RefPtr<GroupInfo>& GroupInfoPair::GetGroupInfoForPersistenceType( + PersistenceType aPersistenceType) { + switch (aPersistenceType) { + case PERSISTENCE_TYPE_TEMPORARY: + return mTemporaryStorageGroupInfo; + case PERSISTENCE_TYPE_DEFAULT: + return mDefaultStorageGroupInfo; + + case PERSISTENCE_TYPE_PERSISTENT: + case PERSISTENCE_TYPE_INVALID: + default: + MOZ_CRASH("Bad persistence type value!"); + } +} + +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<DirectoryLockImpl>>& 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<DirectoryLockImpl>> locks; + uint64_t sizeToBeFreed = + quotaManager->CollectOriginsForEviction(mMinSizeToBeFreed, locks); + + MutexAutoLock lock(mMutex); + + NS_ASSERTION(mWaiting, "Huh?!"); + + mLocks.SwapElements(locks); + mSizeToBeFreed = sizeToBeFreed; + mWaiting = false; + mCondVar.Notify(); + + return NS_OK; +} + +/******************************************************************************* + * OriginOperationBase + ******************************************************************************/ + +NS_IMETHODIMP +OriginOperationBase::Run() { + nsresult rv; + + switch (mState) { + case State_Initial: { + rv = Init(); + break; + } + + case State_CreatingQuotaManager: { + rv = QuotaManagerOpen(); + break; + } + + case State_DirectoryOpenPending: { + rv = DirectoryOpen(); + break; + } + + case State_DirectoryWorkOpen: { + rv = DirectoryWork(); + break; + } + + case State_UnblockingOpen: { + UnblockOpen(); + return NS_OK; + } + + default: + MOZ_CRASH("Bad state!"); + } + + if (NS_WARN_IF(NS_FAILED(rv)) && mState != State_UnblockingOpen) { + Finish(rv); + } + + return NS_OK; +} + +nsresult OriginOperationBase::DirectoryOpen() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State_DirectoryOpenPending); + + QuotaManager* const quotaManager = QuotaManager::Get(); + QM_TRY(OkIf(quotaManager), NS_ERROR_FAILURE); + + // Must set this before dispatching otherwise we will race with the IO thread. + AdvanceState(); + + QM_TRY(quotaManager->IOThread()->Dispatch(this, NS_DISPATCH_NORMAL), + NS_ERROR_FAILURE); + + return NS_OK; +} + +void OriginOperationBase::Finish(nsresult aResult) { + if (NS_SUCCEEDED(mResultCode)) { + mResultCode = aResult; + } + + // Must set mState before dispatching otherwise we will race with the main + // thread. + mState = State_UnblockingOpen; + + MOZ_ALWAYS_SUCCEEDS(mOwningThread->Dispatch(this, NS_DISPATCH_NORMAL)); +} + +nsresult OriginOperationBase::Init() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State_Initial); + + if (QuotaManager::IsShuttingDown()) { + return NS_ERROR_FAILURE; + } + + AdvanceState(); + + if (mNeedsQuotaManagerInit && !QuotaManager::Get()) { + QuotaManager::GetOrCreate(this); + } else { + Open(); + } + + return NS_OK; +} + +nsresult OriginOperationBase::QuotaManagerOpen() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State_CreatingQuotaManager); + + if (NS_WARN_IF(!QuotaManager::Get())) { + return NS_ERROR_FAILURE; + } + + Open(); + + return NS_OK; +} + +nsresult OriginOperationBase::DirectoryWork() { + AssertIsOnIOThread(); + MOZ_ASSERT(mState == State_DirectoryWorkOpen); + + QuotaManager* const quotaManager = QuotaManager::Get(); + QM_TRY(OkIf(quotaManager), NS_ERROR_FAILURE); + + if (mNeedsStorageInit) { + QM_TRY(quotaManager->EnsureStorageIsInitialized()); + } + + QM_TRY(DoDirectoryWork(*quotaManager)); + + // Must set mState before dispatching otherwise we will race with the owning + // thread. + AdvanceState(); + + MOZ_ALWAYS_SUCCEEDS(mOwningThread->Dispatch(this, NS_DISPATCH_NORMAL)); + + return NS_OK; +} + +void FinalizeOriginEvictionOp::Dispatch() { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(GetState() == State_Initial); + + SetState(State_DirectoryOpenPending); + + MOZ_ALWAYS_SUCCEEDS(mOwningThread->Dispatch(this, NS_DISPATCH_NORMAL)); +} + +void FinalizeOriginEvictionOp::RunOnIOThreadImmediately() { + AssertIsOnIOThread(); + MOZ_ASSERT(GetState() == State_Initial); + + SetState(State_DirectoryWorkOpen); + + MOZ_ALWAYS_SUCCEEDS(this->Run()); +} + +void FinalizeOriginEvictionOp::Open() { MOZ_CRASH("Shouldn't get here!"); } + +nsresult FinalizeOriginEvictionOp::DoDirectoryWork( + QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + + AUTO_PROFILER_LABEL("FinalizeOriginEvictionOp::DoDirectoryWork", OTHER); + + for (RefPtr<DirectoryLockImpl>& lock : mLocks) { + aQuotaManager.OriginClearCompleted( + lock->GetPersistenceType(), lock->Origin(), Nullable<Client::Type>()); + } + + return NS_OK; +} + +void FinalizeOriginEvictionOp::UnblockOpen() { + AssertIsOnOwningThread(); + MOZ_ASSERT(GetState() == State_UnblockingOpen); + +#ifdef DEBUG + NoteActorDestroyed(); +#endif + + mLocks.Clear(); + + AdvanceState(); +} + +NS_IMPL_ISUPPORTS_INHERITED0(NormalOriginOperationBase, Runnable) + +void NormalOriginOperationBase::Open() { + AssertIsOnOwningThread(); + MOZ_ASSERT(GetState() == State_CreatingQuotaManager); + MOZ_ASSERT(QuotaManager::Get()); + + AdvanceState(); + + if (mNeedsDirectoryLocking) { + RefPtr<DirectoryLock> pendingDirectoryLock = + QuotaManager::Get()->OpenDirectoryInternal( + mPersistenceType, mOriginScope, mClientType, mExclusive, this); + } else { + QM_TRY(DirectoryOpen(), QM_VOID, [this](const nsresult rv) { Finish(rv); }); + } +} + +void NormalOriginOperationBase::UnblockOpen() { + AssertIsOnOwningThread(); + MOZ_ASSERT(GetState() == State_UnblockingOpen); + + SendResults(); + + if (mNeedsDirectoryLocking) { + mDirectoryLock = nullptr; + } + + UnregisterNormalOriginOp(*this); + + AdvanceState(); +} + +void NormalOriginOperationBase::DirectoryLockAcquired(DirectoryLock* aLock) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aLock); + MOZ_ASSERT(GetState() == State_DirectoryOpenPending); + MOZ_ASSERT(!mDirectoryLock); + + mDirectoryLock = aLock; + + QM_TRY(DirectoryOpen(), QM_VOID, [this](const nsresult rv) { Finish(rv); }); +} + +void NormalOriginOperationBase::DirectoryLockFailed() { + AssertIsOnOwningThread(); + MOZ_ASSERT(GetState() == State_DirectoryOpenPending); + MOZ_ASSERT(!mDirectoryLock); + + Finish(NS_ERROR_FAILURE); +} + +nsresult SaveOriginAccessTimeOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + MOZ_ASSERT(!mPersistenceType.IsNull()); + MOZ_ASSERT(mOriginScope.IsOrigin()); + + AUTO_PROFILER_LABEL("SaveOriginAccessTimeOp::DoDirectoryWork", OTHER); + + QM_TRY_INSPECT(const auto& file, + aQuotaManager.GetDirectoryForOrigin(mPersistenceType.Value(), + mOriginScope.GetOrigin())); + + // The origin directory might not exist + // anymore, because it was deleted by a clear operation. + QM_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE(file, Exists)); + + if (exists) { + QM_TRY(file->Append(nsLiteralString(METADATA_V2_FILE_NAME))); + + QM_TRY_INSPECT(const auto& stream, + GetBinaryOutputStream(*file, kUpdateFileFlag)); + MOZ_ASSERT(stream); + + QM_TRY(stream->Write64(mTimestamp)); + } + + return NS_OK; +} + +void SaveOriginAccessTimeOp::SendResults() { +#ifdef DEBUG + NoteActorDestroyed(); +#endif +} + +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; +} + +void RecordQuotaInfoLoadTimeHelper::Start() { + AssertIsOnIOThread(); + + // XXX: If a OS sleep/wake occur after mStartTime is initialized but before + // gLastOSWake is set, then this time duration would still be recorded with + // key "Normal". We are assumming this is rather rare to happen. + mStartTime.init(TimeStamp::Now()); + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(this)); +} + +void RecordQuotaInfoLoadTimeHelper::End() { + AssertIsOnIOThread(); + + mEndTime.init(TimeStamp::Now()); + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(this)); +} + +NS_IMETHODIMP +RecordQuotaInfoLoadTimeHelper::Run() { + MOZ_ASSERT(NS_IsMainThread()); + + if (mInitializedTime.isSome()) { + // Keys for QM_QUOTA_INFO_LOAD_TIME_V0: + // Normal: Normal conditions. + // WasSuspended: There was a OS sleep so that it was suspended. + // TimeStampErr1: The recorded start time is unexpectedly greater than the + // end time. + // TimeStampErr2: The initialized time for the recording class is unexpectly + // greater than the last OS wake time. + const auto key = [this, wasSuspended = gLastOSWake > *mInitializedTime]() { + if (wasSuspended) { + return "WasSuspended"_ns; + } + + // XXX File a bug if we have data for this key. + // We found negative values in our query in STMO for + // ScalarID::QM_REPOSITORIES_INITIALIZATION_TIME. This shouldn't happen + // because the documentation for TimeStamp::Now() says it returns a + // monotonically increasing number. + if (*mStartTime > *mEndTime) { + return "TimeStampErr1"_ns; + } + + if (*mInitializedTime > gLastOSWake) { + return "TimeStampErr2"_ns; + } + + return "Normal"_ns; + }(); + + Telemetry::AccumulateTimeDelta(Telemetry::QM_QUOTA_INFO_LOAD_TIME_V0, key, + *mStartTime, *mEndTime); + + return NS_OK; + } + + gLastOSWake = TimeStamp::Now(); + mInitializedTime.init(gLastOSWake); + + return NS_OK; +} + +/******************************************************************************* + * Quota + ******************************************************************************/ + +Quota::Quota() +#ifdef DEBUG + : mActorDestroyed(false) +#endif +{ +} + +Quota::~Quota() { MOZ_ASSERT(mActorDestroyed); } + +void Quota::StartIdleMaintenance() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!QuotaManager::IsShuttingDown()); + + QuotaManager* const quotaManager = QuotaManager::Get(); + QM_TRY(OkIf(quotaManager), QM_VOID); + + quotaManager->StartIdleMaintenance(); +} + +bool Quota::VerifyRequestParams(const UsageRequestParams& aParams) const { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParams.type() != UsageRequestParams::T__None); + + switch (aParams.type()) { + case UsageRequestParams::TAllUsageParams: + break; + + case UsageRequestParams::TOriginUsageParams: { + const OriginUsageParams& params = aParams.get_OriginUsageParams(); + + if (NS_WARN_IF( + !QuotaManager::IsPrincipalInfoValid(params.principalInfo()))) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + break; + } + + default: + MOZ_CRASH("Should never get here!"); + } + + return true; +} + +bool Quota::VerifyRequestParams(const RequestParams& aParams) const { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParams.type() != RequestParams::T__None); + + switch (aParams.type()) { + case RequestParams::TStorageNameParams: + case RequestParams::TStorageInitializedParams: + case RequestParams::TTemporaryStorageInitializedParams: + case RequestParams::TInitParams: + case RequestParams::TInitTemporaryStorageParams: + break; + + case RequestParams::TInitializePersistentOriginParams: { + const InitializePersistentOriginParams& params = + aParams.get_InitializePersistentOriginParams(); + + if (NS_WARN_IF( + !QuotaManager::IsPrincipalInfoValid(params.principalInfo()))) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + break; + } + + case RequestParams::TInitializeTemporaryOriginParams: { + const InitializeTemporaryOriginParams& params = + aParams.get_InitializeTemporaryOriginParams(); + + if (NS_WARN_IF(!IsBestEffortPersistenceType(params.persistenceType()))) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + if (NS_WARN_IF( + !QuotaManager::IsPrincipalInfoValid(params.principalInfo()))) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + break; + } + + case RequestParams::TClearOriginParams: { + const ClearResetOriginParams& params = + aParams.get_ClearOriginParams().commonParams(); + + if (NS_WARN_IF( + !QuotaManager::IsPrincipalInfoValid(params.principalInfo()))) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + if (params.persistenceTypeIsExplicit()) { + if (NS_WARN_IF(!IsValidPersistenceType(params.persistenceType()))) { + ASSERT_UNLESS_FUZZING(); + return false; + } + } + + if (params.clientTypeIsExplicit()) { + if (NS_WARN_IF(!Client::IsValidType(params.clientType()))) { + ASSERT_UNLESS_FUZZING(); + return false; + } + } + + break; + } + + case RequestParams::TResetOriginParams: { + const ClearResetOriginParams& params = + aParams.get_ResetOriginParams().commonParams(); + + if (NS_WARN_IF( + !QuotaManager::IsPrincipalInfoValid(params.principalInfo()))) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + if (params.persistenceTypeIsExplicit()) { + if (NS_WARN_IF(!IsValidPersistenceType(params.persistenceType()))) { + ASSERT_UNLESS_FUZZING(); + return false; + } + } + + if (params.clientTypeIsExplicit()) { + if (NS_WARN_IF(!Client::IsValidType(params.clientType()))) { + ASSERT_UNLESS_FUZZING(); + return false; + } + } + + break; + } + + case RequestParams::TClearDataParams: { + if (BackgroundParent::IsOtherProcessActor(Manager())) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + break; + } + + case RequestParams::TClearAllParams: + case RequestParams::TResetAllParams: + case RequestParams::TListOriginsParams: + break; + + case RequestParams::TPersistedParams: { + const PersistedParams& params = aParams.get_PersistedParams(); + + if (NS_WARN_IF( + !QuotaManager::IsPrincipalInfoValid(params.principalInfo()))) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + break; + } + + case RequestParams::TPersistParams: { + const PersistParams& params = aParams.get_PersistParams(); + + if (NS_WARN_IF( + !QuotaManager::IsPrincipalInfoValid(params.principalInfo()))) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + break; + } + + case RequestParams::TEstimateParams: { + const EstimateParams& params = aParams.get_EstimateParams(); + + if (NS_WARN_IF( + !QuotaManager::IsPrincipalInfoValid(params.principalInfo()))) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + break; + } + + default: + MOZ_CRASH("Should never get here!"); + } + + return true; +} + +void Quota::ActorDestroy(ActorDestroyReason aWhy) { + AssertIsOnBackgroundThread(); +#ifdef DEBUG + MOZ_ASSERT(!mActorDestroyed); + mActorDestroyed = true; +#endif +} + +PQuotaUsageRequestParent* Quota::AllocPQuotaUsageRequestParent( + const UsageRequestParams& aParams) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParams.type() != UsageRequestParams::T__None); + + if (NS_WARN_IF(QuotaManager::IsShuttingDown())) { + return nullptr; + } + +#ifdef DEBUG + // Always verify parameters in DEBUG builds! + bool trustParams = false; +#else + bool trustParams = !BackgroundParent::IsOtherProcessActor(Manager()); +#endif + + if (!trustParams && NS_WARN_IF(!VerifyRequestParams(aParams))) { + ASSERT_UNLESS_FUZZING(); + return nullptr; + } + + auto actor = [&]() -> RefPtr<QuotaUsageRequestBase> { + switch (aParams.type()) { + case UsageRequestParams::TAllUsageParams: + return MakeRefPtr<GetUsageOp>(aParams); + + case UsageRequestParams::TOriginUsageParams: + return MakeRefPtr<GetOriginUsageOp>(aParams); + + default: + MOZ_CRASH("Should never get here!"); + } + }(); + + MOZ_ASSERT(actor); + + RegisterNormalOriginOp(*actor); + + // Transfer ownership to IPDL. + return actor.forget().take(); +} + +mozilla::ipc::IPCResult Quota::RecvPQuotaUsageRequestConstructor( + PQuotaUsageRequestParent* aActor, const UsageRequestParams& aParams) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(aParams.type() != UsageRequestParams::T__None); + MOZ_ASSERT(!QuotaManager::IsShuttingDown()); + + auto* op = static_cast<QuotaUsageRequestBase*>(aActor); + + op->Init(*this); + + op->RunImmediately(); + return IPC_OK(); +} + +bool Quota::DeallocPQuotaUsageRequestParent(PQuotaUsageRequestParent* aActor) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + // Transfer ownership back from IPDL. + RefPtr<QuotaUsageRequestBase> actor = + dont_AddRef(static_cast<QuotaUsageRequestBase*>(aActor)); + return true; +} + +PQuotaRequestParent* Quota::AllocPQuotaRequestParent( + const RequestParams& aParams) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParams.type() != RequestParams::T__None); + + if (NS_WARN_IF(QuotaManager::IsShuttingDown())) { + return nullptr; + } + +#ifdef DEBUG + // Always verify parameters in DEBUG builds! + bool trustParams = false; +#else + bool trustParams = !BackgroundParent::IsOtherProcessActor(Manager()); +#endif + + if (!trustParams && NS_WARN_IF(!VerifyRequestParams(aParams))) { + ASSERT_UNLESS_FUZZING(); + return nullptr; + } + + auto actor = [&]() -> RefPtr<QuotaRequestBase> { + switch (aParams.type()) { + case RequestParams::TStorageNameParams: + return MakeRefPtr<StorageNameOp>(); + + case RequestParams::TStorageInitializedParams: + return MakeRefPtr<StorageInitializedOp>(); + + case RequestParams::TTemporaryStorageInitializedParams: + return MakeRefPtr<TemporaryStorageInitializedOp>(); + + case RequestParams::TInitParams: + return MakeRefPtr<InitOp>(); + + case RequestParams::TInitTemporaryStorageParams: + return MakeRefPtr<InitTemporaryStorageOp>(); + + case RequestParams::TInitializePersistentOriginParams: + return MakeRefPtr<InitializePersistentOriginOp>(aParams); + + case RequestParams::TInitializeTemporaryOriginParams: + return MakeRefPtr<InitializeTemporaryOriginOp>(aParams); + + case RequestParams::TClearOriginParams: + return MakeRefPtr<ClearOriginOp>(aParams); + + case RequestParams::TResetOriginParams: + return MakeRefPtr<ResetOriginOp>(aParams); + + case RequestParams::TClearDataParams: + return MakeRefPtr<ClearDataOp>(aParams); + + case RequestParams::TClearAllParams: + return MakeRefPtr<ResetOrClearOp>(/* aClear */ true); + + case RequestParams::TResetAllParams: + return MakeRefPtr<ResetOrClearOp>(/* aClear */ false); + + case RequestParams::TPersistedParams: + return MakeRefPtr<PersistedOp>(aParams); + + case RequestParams::TPersistParams: + return MakeRefPtr<PersistOp>(aParams); + + case RequestParams::TEstimateParams: + return MakeRefPtr<EstimateOp>(aParams); + + case RequestParams::TListOriginsParams: + return MakeRefPtr<ListOriginsOp>(); + + default: + MOZ_CRASH("Should never get here!"); + } + }(); + + MOZ_ASSERT(actor); + + RegisterNormalOriginOp(*actor); + + // Transfer ownership to IPDL. + return actor.forget().take(); +} + +mozilla::ipc::IPCResult Quota::RecvPQuotaRequestConstructor( + PQuotaRequestParent* aActor, const RequestParams& aParams) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(aParams.type() != RequestParams::T__None); + MOZ_ASSERT(!QuotaManager::IsShuttingDown()); + + auto* op = static_cast<QuotaRequestBase*>(aActor); + + op->Init(*this); + + op->RunImmediately(); + return IPC_OK(); +} + +bool Quota::DeallocPQuotaRequestParent(PQuotaRequestParent* aActor) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + // Transfer ownership back from IPDL. + RefPtr<QuotaRequestBase> actor = + dont_AddRef(static_cast<QuotaRequestBase*>(aActor)); + return true; +} + +mozilla::ipc::IPCResult Quota::RecvStartIdleMaintenance() { + AssertIsOnBackgroundThread(); + + PBackgroundParent* actor = Manager(); + MOZ_ASSERT(actor); + + if (BackgroundParent::IsOtherProcessActor(actor)) { + ASSERT_UNLESS_FUZZING(); + return IPC_FAIL_NO_REASON(this); + } + + if (QuotaManager::IsShuttingDown()) { + return IPC_OK(); + } + + QuotaManager* quotaManager = QuotaManager::Get(); + if (!quotaManager) { + nsCOMPtr<nsIRunnable> callback = + NewRunnableMethod("dom::quota::Quota::StartIdleMaintenance", this, + &Quota::StartIdleMaintenance); + + QuotaManager::GetOrCreate(callback); + return IPC_OK(); + } + + quotaManager->StartIdleMaintenance(); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult Quota::RecvStopIdleMaintenance() { + AssertIsOnBackgroundThread(); + + PBackgroundParent* actor = Manager(); + MOZ_ASSERT(actor); + + if (BackgroundParent::IsOtherProcessActor(actor)) { + ASSERT_UNLESS_FUZZING(); + return IPC_FAIL_NO_REASON(this); + } + + if (QuotaManager::IsShuttingDown()) { + return IPC_OK(); + } + + QuotaManager* quotaManager = QuotaManager::Get(); + if (!quotaManager) { + return IPC_OK(); + } + + quotaManager->StopIdleMaintenance(); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult Quota::RecvAbortOperationsForProcess( + const ContentParentId& aContentParentId) { + AssertIsOnBackgroundThread(); + + PBackgroundParent* actor = Manager(); + MOZ_ASSERT(actor); + + if (BackgroundParent::IsOtherProcessActor(actor)) { + ASSERT_UNLESS_FUZZING(); + return IPC_FAIL_NO_REASON(this); + } + + if (QuotaManager::IsShuttingDown()) { + return IPC_OK(); + } + + QuotaManager* quotaManager = QuotaManager::Get(); + if (!quotaManager) { + return IPC_OK(); + } + + quotaManager->AbortOperationsForProcess(aContentParentId); + + return IPC_OK(); +} + +void QuotaUsageRequestBase::Init(Quota& aQuota) { + AssertIsOnOwningThread(); + + mNeedsQuotaManagerInit = true; + mNeedsStorageInit = true; +} + +Result<UsageInfo, nsresult> QuotaUsageRequestBase::GetUsageForOrigin( + QuotaManager& aQuotaManager, PersistenceType aPersistenceType, + const GroupAndOrigin& aGroupAndOrigin) { + AssertIsOnIOThread(); + + QM_TRY_INSPECT(const auto& directory, + aQuotaManager.GetDirectoryForOrigin(aPersistenceType, + aGroupAndOrigin.mOrigin)); + + QM_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE(directory, Exists)); + + if (!exists || mCanceled) { + return UsageInfo(); + } + + // If the directory exists then enumerate all the files inside, adding up + // the sizes to get the final usage statistic. + bool initialized; + + if (aPersistenceType == PERSISTENCE_TYPE_PERSISTENT) { + initialized = aQuotaManager.IsOriginInitialized(aGroupAndOrigin.mOrigin); + } else { + initialized = aQuotaManager.IsTemporaryStorageInitialized(); + } + + return GetUsageForOriginEntries(aQuotaManager, aPersistenceType, + aGroupAndOrigin, *directory, initialized); +} + +Result<UsageInfo, nsresult> QuotaUsageRequestBase::GetUsageForOriginEntries( + QuotaManager& aQuotaManager, PersistenceType aPersistenceType, + const GroupAndOrigin& aGroupAndOrigin, nsIFile& aDirectory, + const bool aInitialized) { + AssertIsOnIOThread(); + + QM_TRY_RETURN((ReduceEachFileAtomicCancelable( + aDirectory, mCanceled, UsageInfo{}, + [&](UsageInfo oldUsageInfo, const nsCOMPtr<nsIFile>& file) + -> mozilla::Result<UsageInfo, nsresult> { + QM_TRY_INSPECT(const bool& isDirectory, + MOZ_TO_RESULT_INVOKE(file, IsDirectory)); + + QM_TRY_INSPECT( + const auto& leafName, + MOZ_TO_RESULT_INVOKE_TYPED(nsAutoString, file, GetLeafName)); + + if (!isDirectory) { + // We are maintaining existing behavior for unknown files here (just + // continuing). + // This can possibly be used by developers to add temporary backups + // into origin directories without losing get usage functionality. + if (IsTempMetadata(leafName)) { + if (!aInitialized) { + QM_TRY(file->Remove(/* recursive */ false)); + } + + return oldUsageInfo; + } + + if (IsOriginMetadata(leafName) || IsOSMetadata(leafName) || + IsDotFile(leafName)) { + return oldUsageInfo; + } + + // Unknown files during getting usage for an origin (even for an + // uninitialized origin) are now allowed. Just warn if we find them. + UNKNOWN_FILE_WARNING(leafName); + return oldUsageInfo; + } + + Client::Type clientType; + const bool ok = Client::TypeFromText(leafName, clientType, fallible); + if (!ok) { + // Unknown directories during getting usage for an origin (even for an + // uninitialized origin) are now allowed. Just warn if we find them. + UNKNOWN_FILE_WARNING(leafName); + return oldUsageInfo; + } + + Client* const client = aQuotaManager.GetClient(clientType); + MOZ_ASSERT(client); + + QM_TRY_INSPECT(const auto& usageInfo, + aInitialized + ? client->GetUsageForOrigin( + aPersistenceType, aGroupAndOrigin, mCanceled) + : client->InitOrigin(aPersistenceType, + aGroupAndOrigin, mCanceled)); + return oldUsageInfo + usageInfo; + }))); +} + +void QuotaUsageRequestBase::SendResults() { + AssertIsOnOwningThread(); + + if (IsActorDestroyed()) { + if (NS_SUCCEEDED(mResultCode)) { + mResultCode = NS_ERROR_FAILURE; + } + } else { + if (mCanceled) { + mResultCode = NS_ERROR_FAILURE; + } + + UsageRequestResponse response; + + if (NS_SUCCEEDED(mResultCode)) { + GetResponse(response); + } else { + response = mResultCode; + } + + Unused << PQuotaUsageRequestParent::Send__delete__(this, response); + } +} + +void QuotaUsageRequestBase::ActorDestroy(ActorDestroyReason aWhy) { + AssertIsOnOwningThread(); + + NoteActorDestroyed(); +} + +mozilla::ipc::IPCResult QuotaUsageRequestBase::RecvCancel() { + AssertIsOnOwningThread(); + + if (mCanceled.exchange(true)) { + NS_WARNING("Canceled more than once?!"); + return IPC_FAIL_NO_REASON(this); + } + + return IPC_OK(); +} + +nsresult TraverseRepositoryHelper::TraverseRepository( + QuotaManager& aQuotaManager, PersistenceType aPersistenceType) { + AssertIsOnIOThread(); + + QM_TRY_INSPECT( + const auto& directory, + QM_NewLocalFile(aQuotaManager.GetStoragePath(aPersistenceType))); + + QM_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE(directory, Exists)); + + if (!exists) { + return NS_OK; + } + + QM_TRY(CollectEachFileAtomicCancelable( + *directory, GetIsCanceledFlag(), + [this, aPersistenceType, &aQuotaManager, + persistent = aPersistenceType == PERSISTENCE_TYPE_PERSISTENT]( + const nsCOMPtr<nsIFile>& originDir) -> Result<Ok, nsresult> { + QM_TRY_INSPECT(const bool& isDirectory, + MOZ_TO_RESULT_INVOKE(originDir, IsDirectory)); + + if (!isDirectory) { + QM_TRY_INSPECT( + const auto& leafName, + MOZ_TO_RESULT_INVOKE_TYPED(nsAutoString, originDir, GetLeafName)); + + // Unknown files during getting usages are allowed. Just warn if we + // find them. + if (!IsOSMetadata(leafName)) { + UNKNOWN_FILE_WARNING(leafName); + } + return Ok{}; + } + + QM_TRY(ProcessOrigin(aQuotaManager, *originDir, persistent, + aPersistenceType)); + + return Ok{}; + })); + + return NS_OK; +} + +GetUsageOp::GetUsageOp(const UsageRequestParams& aParams) + : mGetAll(aParams.get_AllUsageParams().getAll()) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aParams.type() == UsageRequestParams::TAllUsageParams); +} + +void GetUsageOp::ProcessOriginInternal(QuotaManager* aQuotaManager, + const PersistenceType aPersistenceType, + const nsACString& aOrigin, + const int64_t aTimestamp, + const bool aPersisted, + const uint64_t aUsage) { + if (!mGetAll && aQuotaManager->IsOriginInternal(aOrigin)) { + return; + } + + OriginUsage* originUsage; + + // We can't store pointers to OriginUsage objects in the hashtable + // since AppendElement() reallocates its internal array buffer as number + // of elements grows. + uint32_t index; + if (mOriginUsagesIndex.Get(aOrigin, &index)) { + originUsage = &mOriginUsages[index]; + } else { + index = mOriginUsages.Length(); + + originUsage = mOriginUsages.AppendElement(); + + originUsage->origin() = aOrigin; + originUsage->persisted() = false; + originUsage->usage() = 0; + originUsage->lastAccessed() = 0; + + mOriginUsagesIndex.Put(aOrigin, index); + } + + if (aPersistenceType == PERSISTENCE_TYPE_DEFAULT) { + originUsage->persisted() = aPersisted; + } + + originUsage->usage() = originUsage->usage() + aUsage; + + originUsage->lastAccessed() = + std::max<int64_t>(originUsage->lastAccessed(), aTimestamp); +} + +const Atomic<bool>& GetUsageOp::GetIsCanceledFlag() { + AssertIsOnIOThread(); + + return mCanceled; +} + +nsresult GetUsageOp::ProcessOrigin(QuotaManager& aQuotaManager, + nsIFile& aOriginDir, const bool aPersistent, + const PersistenceType aPersistenceType) { + AssertIsOnIOThread(); + + QM_TRY_INSPECT(const auto& metadata, + aQuotaManager.GetDirectoryMetadataWithQuotaInfo2WithRestore( + &aOriginDir, aPersistent)); + + QM_TRY_INSPECT( + const auto& usageInfo, + GetUsageForOrigin(aQuotaManager, aPersistenceType, metadata.mQuotaInfo)); + + ProcessOriginInternal(&aQuotaManager, aPersistenceType, + metadata.mQuotaInfo.mOrigin, metadata.mTimestamp, + metadata.mPersisted, usageInfo.TotalUsage().valueOr(0)); + + return NS_OK; +} + +nsresult GetUsageOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + aQuotaManager.AssertStorageIsInitialized(); + + AUTO_PROFILER_LABEL("GetUsageOp::DoDirectoryWork", OTHER); + + nsresult rv; + + for (const PersistenceType type : kAllPersistenceTypes) { + rv = TraverseRepository(aQuotaManager, type); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + // TraverseRepository above only consulted the filesystem. We also need to + // consider origins which may have pending quota usage, such as buffered + // LocalStorage writes for an origin which didn't previously have any + // LocalStorage data. + + aQuotaManager.CollectPendingOriginsForListing( + [this, &aQuotaManager](const auto& originInfo) { + ProcessOriginInternal( + &aQuotaManager, originInfo->GetGroupInfo()->GetPersistenceType(), + originInfo->Origin(), originInfo->LockedAccessTime(), + originInfo->LockedPersisted(), originInfo->LockedUsage()); + }); + + return NS_OK; +} + +void GetUsageOp::GetResponse(UsageRequestResponse& aResponse) { + AssertIsOnOwningThread(); + + aResponse = AllUsageResponse(); + + aResponse.get_AllUsageResponse().originUsages() = std::move(mOriginUsages); +} + +GetOriginUsageOp::GetOriginUsageOp(const UsageRequestParams& aParams) + : mUsage(0), mFileUsage(0) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aParams.type() == UsageRequestParams::TOriginUsageParams); + + const OriginUsageParams& params = aParams.get_OriginUsageParams(); + + QuotaInfo quotaInfo = + QuotaManager::GetInfoFromValidatedPrincipalInfo(params.principalInfo()); + + mSuffix = std::move(quotaInfo.mSuffix); + mGroup = std::move(quotaInfo.mGroup); + mOriginScope.SetFromOrigin(quotaInfo.mOrigin); + + mFromMemory = params.fromMemory(); + + // Overwrite NormalOriginOperationBase default values. + if (mFromMemory) { + mNeedsDirectoryLocking = false; + } + + // Overwrite OriginOperationBase default values. + mNeedsQuotaManagerInit = true; + mNeedsStorageInit = true; +} + +nsresult GetOriginUsageOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + aQuotaManager.AssertStorageIsInitialized(); + MOZ_ASSERT(mUsage == 0); + MOZ_ASSERT(mFileUsage == 0); + + AUTO_PROFILER_LABEL("GetOriginUsageOp::DoDirectoryWork", OTHER); + + const GroupAndOrigin groupAndOrigin = {mGroup, + nsCString{mOriginScope.GetOrigin()}}; + + if (mFromMemory) { + // Ensure temporary storage is initialized. If temporary storage hasn't been + // initialized yet, the method will initialize it by traversing the + // repositories for temporary and default storage (including our origin). + QM_TRY(aQuotaManager.EnsureTemporaryStorageIsInitialized()); + + // Get cached usage (the method doesn't have to stat any files). File usage + // is not tracked in memory separately, so just add to the total usage. + mUsage = aQuotaManager.GetOriginUsage(groupAndOrigin); + + return NS_OK; + } + + UsageInfo usageInfo; + + // Add all the persistent/temporary/default storage files we care about. + for (const PersistenceType type : kAllPersistenceTypes) { + auto usageInfoOrErr = + GetUsageForOrigin(aQuotaManager, type, groupAndOrigin); + if (NS_WARN_IF(usageInfoOrErr.isErr())) { + return usageInfoOrErr.unwrapErr(); + } + + usageInfo += usageInfoOrErr.unwrap(); + } + + mUsage = usageInfo.TotalUsage().valueOr(0); + mFileUsage = usageInfo.FileUsage().valueOr(0); + + return NS_OK; +} + +void GetOriginUsageOp::GetResponse(UsageRequestResponse& aResponse) { + AssertIsOnOwningThread(); + + OriginUsageResponse usageResponse; + + usageResponse.usage() = mUsage; + usageResponse.fileUsage() = mFileUsage; + + aResponse = usageResponse; +} + +void QuotaRequestBase::Init(Quota& aQuota) { + AssertIsOnOwningThread(); + + mNeedsQuotaManagerInit = true; + mNeedsStorageInit = true; +} + +void QuotaRequestBase::SendResults() { + AssertIsOnOwningThread(); + + if (IsActorDestroyed()) { + if (NS_SUCCEEDED(mResultCode)) { + mResultCode = NS_ERROR_FAILURE; + } + } else { + RequestResponse response; + + if (NS_SUCCEEDED(mResultCode)) { + GetResponse(response); + } else { + response = mResultCode; + } + + Unused << PQuotaRequestParent::Send__delete__(this, response); + } +} + +void QuotaRequestBase::ActorDestroy(ActorDestroyReason aWhy) { + AssertIsOnOwningThread(); + + NoteActorDestroyed(); +} + +StorageNameOp::StorageNameOp() : QuotaRequestBase(/* aExclusive */ false) { + AssertIsOnOwningThread(); + + // Overwrite NormalOriginOperationBase default values. + mNeedsDirectoryLocking = false; + + // Overwrite OriginOperationBase default values. + mNeedsQuotaManagerInit = true; + mNeedsStorageInit = false; +} + +void StorageNameOp::Init(Quota& aQuota) { AssertIsOnOwningThread(); } + +nsresult StorageNameOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + + AUTO_PROFILER_LABEL("StorageNameOp::DoDirectoryWork", OTHER); + + mName = aQuotaManager.GetStorageName(); + + return NS_OK; +} + +void StorageNameOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + StorageNameResponse storageNameResponse; + + storageNameResponse.name() = mName; + + aResponse = storageNameResponse; +} + +InitializedRequestBase::InitializedRequestBase() + : QuotaRequestBase(/* aExclusive */ false), mInitialized(false) { + AssertIsOnOwningThread(); + + // Overwrite NormalOriginOperationBase default values. + mNeedsDirectoryLocking = false; + + // Overwrite OriginOperationBase default values. + mNeedsQuotaManagerInit = true; + mNeedsStorageInit = false; +} + +void InitializedRequestBase::Init(Quota& aQuota) { AssertIsOnOwningThread(); } + +nsresult StorageInitializedOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + + AUTO_PROFILER_LABEL("StorageInitializedOp::DoDirectoryWork", OTHER); + + mInitialized = aQuotaManager.IsStorageInitialized(); + + return NS_OK; +} + +void StorageInitializedOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + StorageInitializedResponse storageInitializedResponse; + + storageInitializedResponse.initialized() = mInitialized; + + aResponse = storageInitializedResponse; +} + +nsresult TemporaryStorageInitializedOp::DoDirectoryWork( + QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + + AUTO_PROFILER_LABEL("TemporaryStorageInitializedOp::DoDirectoryWork", OTHER); + + mInitialized = aQuotaManager.IsTemporaryStorageInitialized(); + + return NS_OK; +} + +void TemporaryStorageInitializedOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + TemporaryStorageInitializedResponse temporaryStorageInitializedResponse; + + temporaryStorageInitializedResponse.initialized() = mInitialized; + + aResponse = temporaryStorageInitializedResponse; +} + +InitOp::InitOp() : QuotaRequestBase(/* aExclusive */ false) { + AssertIsOnOwningThread(); + + // Overwrite OriginOperationBase default values. + mNeedsQuotaManagerInit = true; + mNeedsStorageInit = false; +} + +void InitOp::Init(Quota& aQuota) { AssertIsOnOwningThread(); } + +nsresult InitOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + + AUTO_PROFILER_LABEL("InitOp::DoDirectoryWork", OTHER); + + QM_TRY(aQuotaManager.EnsureStorageIsInitialized()); + + return NS_OK; +} + +void InitOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + aResponse = InitResponse(); +} + +InitTemporaryStorageOp::InitTemporaryStorageOp() + : QuotaRequestBase(/* aExclusive */ false) { + AssertIsOnOwningThread(); + + // Overwrite OriginOperationBase default values. + mNeedsQuotaManagerInit = true; + mNeedsStorageInit = false; +} + +void InitTemporaryStorageOp::Init(Quota& aQuota) { AssertIsOnOwningThread(); } + +nsresult InitTemporaryStorageOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + + AUTO_PROFILER_LABEL("InitTemporaryStorageOp::DoDirectoryWork", OTHER); + + QM_TRY(OkIf(aQuotaManager.IsStorageInitialized()), NS_ERROR_FAILURE;); + + QM_TRY(aQuotaManager.EnsureTemporaryStorageIsInitialized()); + + return NS_OK; +} + +void InitTemporaryStorageOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + aResponse = InitTemporaryStorageResponse(); +} + +InitializeOriginRequestBase::InitializeOriginRequestBase( + const PersistenceType aPersistenceType, const PrincipalInfo& aPrincipalInfo) + : QuotaRequestBase(/* aExclusive */ false), mCreated(false) { + AssertIsOnOwningThread(); + + auto quotaInfo = + QuotaManager::GetInfoFromValidatedPrincipalInfo(aPrincipalInfo); + + // Overwrite OriginOperationBase default values. + mNeedsQuotaManagerInit = true; + mNeedsStorageInit = false; + + // Overwrite NormalOriginOperationBase default values. + mPersistenceType.SetValue(aPersistenceType); + mOriginScope.SetFromOrigin(quotaInfo.mOrigin); + + // Overwrite InitializeOriginRequestBase default values. + mSuffix = std::move(quotaInfo.mSuffix); + mGroup = std::move(quotaInfo.mGroup); +} + +void InitializeOriginRequestBase::Init(Quota& aQuota) { + AssertIsOnOwningThread(); +} + +InitializePersistentOriginOp::InitializePersistentOriginOp( + const RequestParams& aParams) + : InitializeOriginRequestBase( + PERSISTENCE_TYPE_PERSISTENT, + aParams.get_InitializePersistentOriginParams().principalInfo()) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aParams.type() == + RequestParams::TInitializePersistentOriginParams); +} + +nsresult InitializePersistentOriginOp::DoDirectoryWork( + QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + MOZ_ASSERT(!mPersistenceType.IsNull()); + + AUTO_PROFILER_LABEL("InitializePersistentOriginOp::DoDirectoryWork", OTHER); + + QM_TRY(OkIf(aQuotaManager.IsStorageInitialized()), NS_ERROR_FAILURE); + + QM_TRY_UNWRAP(mCreated, + (aQuotaManager + .EnsurePersistentOriginIsInitialized(QuotaInfo{ + mSuffix, mGroup, nsCString{mOriginScope.GetOrigin()}}) + .map([](const auto& res) { return res.second; }))); + + return NS_OK; +} + +void InitializePersistentOriginOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + aResponse = InitializePersistentOriginResponse(mCreated); +} + +InitializeTemporaryOriginOp::InitializeTemporaryOriginOp( + const RequestParams& aParams) + : InitializeOriginRequestBase( + aParams.get_InitializeTemporaryOriginParams().persistenceType(), + aParams.get_InitializeTemporaryOriginParams().principalInfo()) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aParams.type() == RequestParams::TInitializeTemporaryOriginParams); +} + +nsresult InitializeTemporaryOriginOp::DoDirectoryWork( + QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + MOZ_ASSERT(!mPersistenceType.IsNull()); + + AUTO_PROFILER_LABEL("InitializeTemporaryOriginOp::DoDirectoryWork", OTHER); + + QM_TRY(OkIf(aQuotaManager.IsStorageInitialized()), NS_ERROR_FAILURE); + + QM_TRY(OkIf(aQuotaManager.IsTemporaryStorageInitialized()), NS_ERROR_FAILURE); + + QM_TRY_UNWRAP( + mCreated, + (aQuotaManager + .EnsureTemporaryOriginIsInitialized( + mPersistenceType.Value(), + QuotaInfo{mSuffix, mGroup, nsCString{mOriginScope.GetOrigin()}}) + .map([](const auto& res) { return res.second; }))); + + return NS_OK; +} + +void InitializeTemporaryOriginOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + aResponse = InitializeTemporaryOriginResponse(mCreated); +} + +ResetOrClearOp::ResetOrClearOp(bool aClear) + : QuotaRequestBase(/* aExclusive */ true), mClear(aClear) { + AssertIsOnOwningThread(); + + // Overwrite OriginOperationBase default values. + mNeedsQuotaManagerInit = true; + mNeedsStorageInit = false; +} + +void ResetOrClearOp::Init(Quota& aQuota) { AssertIsOnOwningThread(); } + +void ResetOrClearOp::DeleteFiles(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + + nsresult rv = aQuotaManager.AboutToClearOrigins(Nullable<PersistenceType>(), + OriginScope::FromNull(), + Nullable<Client::Type>()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + auto directoryOrErr = QM_NewLocalFile(aQuotaManager.GetStoragePath()); + if (NS_WARN_IF(directoryOrErr.isErr())) { + return; + } + + nsCOMPtr<nsIFile> directory = directoryOrErr.unwrap(); + + rv = directory->Remove(true); + if (rv != NS_ERROR_FILE_TARGET_DOES_NOT_EXIST && + rv != NS_ERROR_FILE_NOT_FOUND && NS_FAILED(rv)) { + // This should never fail if we've closed all storage connections + // correctly... + MOZ_ASSERT(false, "Failed to remove storage directory!"); + } +} + +void ResetOrClearOp::DeleteStorageFile(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + + QM_TRY_INSPECT(const auto& storageFile, + QM_NewLocalFile(aQuotaManager.GetBasePath()), QM_VOID); + + QM_TRY(storageFile->Append(aQuotaManager.GetStorageName() + kSQLiteSuffix), + QM_VOID); + + const nsresult rv = storageFile->Remove(true); + if (rv != NS_ERROR_FILE_TARGET_DOES_NOT_EXIST && + rv != NS_ERROR_FILE_NOT_FOUND && NS_FAILED(rv)) { + // This should never fail if we've closed the storage connection + // correctly... + MOZ_ASSERT(false, "Failed to remove storage file!"); + } +} + +nsresult ResetOrClearOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + + AUTO_PROFILER_LABEL("ResetOrClearOp::DoDirectoryWork", OTHER); + + if (mClear) { + DeleteFiles(aQuotaManager); + + aQuotaManager.RemoveQuota(); + } + + aQuotaManager.ShutdownStorage(); + + if (mClear) { + DeleteStorageFile(aQuotaManager); + } + + return NS_OK; +} + +void ResetOrClearOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + if (mClear) { + aResponse = ClearAllResponse(); + } else { + aResponse = ResetAllResponse(); + } +} + +void ClearRequestBase::DeleteFiles(QuotaManager& aQuotaManager, + PersistenceType aPersistenceType) { + AssertIsOnIOThread(); + + QM_TRY(aQuotaManager.AboutToClearOrigins( + Nullable<PersistenceType>(aPersistenceType), mOriginScope, + mClientType), + QM_VOID); + + QM_TRY_INSPECT( + const auto& directory, + QM_NewLocalFile(aQuotaManager.GetStoragePath(aPersistenceType)), QM_VOID); + + nsTArray<nsCOMPtr<nsIFile>> directoriesForRemovalRetry; + + aQuotaManager.MaybeRecordQuotaManagerShutdownStep( + "ClearRequestBase: Starting deleting files"_ns); + + QM_TRY( + CollectEachFile( + *directory, + [originScope = + [this] { + OriginScope originScope = mOriginScope.Clone(); + if (originScope.IsOrigin()) { + originScope.SetOrigin( + MakeSanitizedOriginCString(originScope.GetOrigin())); + } else if (originScope.IsPrefix()) { + originScope.SetOriginNoSuffix(MakeSanitizedOriginCString( + originScope.GetOriginNoSuffix())); + } + return originScope; + }(), + aPersistenceType, &aQuotaManager, &directoriesForRemovalRetry, + this](nsCOMPtr<nsIFile>&& file) -> mozilla::Result<Ok, nsresult> { + QM_TRY_INSPECT(const bool& isDirectory, + MOZ_TO_RESULT_INVOKE(file, IsDirectory)); + + QM_TRY_INSPECT( + const auto& leafName, + MOZ_TO_RESULT_INVOKE_TYPED(nsAutoString, file, GetLeafName)); + + if (!isDirectory) { + // Unknown files during clearing are allowed. Just warn if we find + // them. + if (!IsOSMetadata(leafName)) { + UNKNOWN_FILE_WARNING(leafName); + } + return Ok{}; + } + + // Skip the origin directory if it doesn't match the pattern. + if (!originScope.Matches( + OriginScope::FromOrigin(NS_ConvertUTF16toUTF8(leafName)))) { + return Ok{}; + } + + const bool persistent = + aPersistenceType == PERSISTENCE_TYPE_PERSISTENT; + + QM_TRY_INSPECT( + const auto& metadata, + aQuotaManager.GetDirectoryMetadataWithQuotaInfo2WithRestore( + file, persistent)); + + if (!mClientType.IsNull()) { + nsAutoString clientDirectoryName; + QM_TRY(OkIf(Client::TypeToText(mClientType.Value(), + clientDirectoryName, fallible)), + Err(NS_ERROR_FAILURE)); + + QM_TRY(file->Append(clientDirectoryName)); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE(file, Exists)); + + if (!exists) { + return Ok{}; + } + } + + // We can't guarantee that this will always succeed on + // Windows... + if (NS_FAILED((file->Remove(true)))) { + NS_WARNING("Failed to remove directory, retrying later."); + + directoriesForRemovalRetry.AppendElement(std::move(file)); + } + + const bool initialized = + aPersistenceType == PERSISTENCE_TYPE_PERSISTENT + ? aQuotaManager.IsOriginInitialized( + metadata.mQuotaInfo.mOrigin) + : aQuotaManager.IsTemporaryStorageInitialized(); + + // If it hasn't been initialized, we don't need to update the quota + // and notify the removing client. + if (!initialized) { + return Ok{}; + } + + if (aPersistenceType != PERSISTENCE_TYPE_PERSISTENT) { + if (mClientType.IsNull()) { + aQuotaManager.RemoveQuotaForOrigin(aPersistenceType, + metadata.mQuotaInfo); + } else { + aQuotaManager.ResetUsageForClient( + aPersistenceType, metadata.mQuotaInfo, mClientType.Value()); + } + } + + aQuotaManager.OriginClearCompleted( + aPersistenceType, metadata.mQuotaInfo.mOrigin, mClientType); + + return Ok{}; + }), + QM_VOID); + + // Retry removing any directories that failed to be removed earlier now. + // + // XXX This will still block this operation. We might instead dispatch a + // runnable to our own thread for each retry round with a timer. We must + // ensure that the directory lock is upheld until we complete or give up + // though. + for (uint32_t index = 0; index < 10; index++) { + aQuotaManager.MaybeRecordQuotaManagerShutdownStep( + "ClearRequestBase: Retrying directory removal"_ns); + + for (auto&& file : std::exchange(directoriesForRemovalRetry, + nsTArray<nsCOMPtr<nsIFile>>{})) { + if (NS_FAILED((file->Remove(true)))) { + directoriesForRemovalRetry.AppendElement(std::move(file)); + } + } + + if (directoriesForRemovalRetry.IsEmpty()) { + break; + } + + PR_Sleep(PR_MillisecondsToInterval(200)); + } + + if (!directoriesForRemovalRetry.IsEmpty()) { + NS_WARNING("Failed to remove one or more directories, giving up!"); + } + + aQuotaManager.MaybeRecordQuotaManagerShutdownStep( + "ClearRequestBase: Completed deleting files"_ns); +} + +nsresult ClearRequestBase::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + aQuotaManager.AssertStorageIsInitialized(); + + AUTO_PROFILER_LABEL("ClearRequestBase::DoDirectoryWork", OTHER); + + if (mPersistenceType.IsNull()) { + for (const PersistenceType type : kAllPersistenceTypes) { + DeleteFiles(aQuotaManager, type); + } + } else { + DeleteFiles(aQuotaManager, mPersistenceType.Value()); + } + + return NS_OK; +} + +ClearOriginOp::ClearOriginOp(const RequestParams& aParams) + : ClearRequestBase(/* aExclusive */ true), + mParams(aParams.get_ClearOriginParams().commonParams()), + mMatchAll(aParams.get_ClearOriginParams().matchAll()) { + MOZ_ASSERT(aParams.type() == RequestParams::TClearOriginParams); +} + +void ClearOriginOp::Init(Quota& aQuota) { + AssertIsOnOwningThread(); + + QuotaRequestBase::Init(aQuota); + + if (mParams.persistenceTypeIsExplicit()) { + mPersistenceType.SetValue(mParams.persistenceType()); + } + + // Figure out which origin we're dealing with. + const auto origin = QuotaManager::GetOriginFromValidatedPrincipalInfo( + mParams.principalInfo()); + + if (mMatchAll) { + mOriginScope.SetFromPrefix(origin); + } else { + mOriginScope.SetFromOrigin(origin); + } + + if (mParams.clientTypeIsExplicit()) { + mClientType.SetValue(mParams.clientType()); + } +} + +void ClearOriginOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + aResponse = ClearOriginResponse(); +} + +ClearDataOp::ClearDataOp(const RequestParams& aParams) + : ClearRequestBase(/* aExclusive */ true), mParams(aParams) { + MOZ_ASSERT(aParams.type() == RequestParams::TClearDataParams); +} + +void ClearDataOp::Init(Quota& aQuota) { + AssertIsOnOwningThread(); + + QuotaRequestBase::Init(aQuota); + + mOriginScope.SetFromPattern(mParams.pattern()); +} + +void ClearDataOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + aResponse = ClearDataResponse(); +} + +ResetOriginOp::ResetOriginOp(const RequestParams& aParams) + : QuotaRequestBase(/* aExclusive */ true) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aParams.type() == RequestParams::TResetOriginParams); + + const ClearResetOriginParams& params = + aParams.get_ResetOriginParams().commonParams(); + + const auto origin = + QuotaManager::GetOriginFromValidatedPrincipalInfo(params.principalInfo()); + + // Overwrite OriginOperationBase default values. + mNeedsQuotaManagerInit = true; + mNeedsStorageInit = false; + + // Overwrite NormalOriginOperationBase default values. + if (params.persistenceTypeIsExplicit()) { + mPersistenceType.SetValue(params.persistenceType()); + } + + mOriginScope.SetFromOrigin(origin); + + if (params.clientTypeIsExplicit()) { + mClientType.SetValue(params.clientType()); + } +} + +void ResetOriginOp::Init(Quota& aQuota) { AssertIsOnOwningThread(); } + +nsresult ResetOriginOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + + AUTO_PROFILER_LABEL("ResetOriginOp::DoDirectoryWork", OTHER); + + // All the work is handled by NormalOriginOperationBase parent class. In this + // particular case, we just needed to acquire an exclusive directory lock and + // that's it. + + return NS_OK; +} + +void ResetOriginOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + aResponse = ResetOriginResponse(); +} + +PersistRequestBase::PersistRequestBase(const PrincipalInfo& aPrincipalInfo) + : QuotaRequestBase(/* aExclusive */ false), mPrincipalInfo(aPrincipalInfo) { + AssertIsOnOwningThread(); +} + +void PersistRequestBase::Init(Quota& aQuota) { + AssertIsOnOwningThread(); + + QuotaRequestBase::Init(aQuota); + + mPersistenceType.SetValue(PERSISTENCE_TYPE_DEFAULT); + + // Figure out which origin we're dealing with. + QuotaInfo quotaInfo = + QuotaManager::GetInfoFromValidatedPrincipalInfo(mPrincipalInfo); + + mSuffix = std::move(quotaInfo.mSuffix); + mGroup = std::move(quotaInfo.mGroup); + mOriginScope.SetFromOrigin(quotaInfo.mOrigin); +} + +PersistedOp::PersistedOp(const RequestParams& aParams) + : PersistRequestBase(aParams.get_PersistedParams().principalInfo()), + mPersisted(false) { + MOZ_ASSERT(aParams.type() == RequestParams::TPersistedParams); +} + +nsresult PersistedOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + aQuotaManager.AssertStorageIsInitialized(); + MOZ_ASSERT(!mPersistenceType.IsNull()); + MOZ_ASSERT(mPersistenceType.Value() == PERSISTENCE_TYPE_DEFAULT); + MOZ_ASSERT(mOriginScope.IsOrigin()); + + AUTO_PROFILER_LABEL("PersistedOp::DoDirectoryWork", OTHER); + + Nullable<bool> persisted = aQuotaManager.OriginPersisted( + GroupAndOrigin{mGroup, nsCString{mOriginScope.GetOrigin()}}); + + if (!persisted.IsNull()) { + mPersisted = persisted.Value(); + return NS_OK; + } + + // If we get here, it means the origin hasn't been initialized yet. + // Try to get the persisted flag from directory metadata on disk. + + QM_TRY_INSPECT(const auto& directory, + aQuotaManager.GetDirectoryForOrigin(mPersistenceType.Value(), + mOriginScope.GetOrigin())); + + QM_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE(directory, Exists)); + + if (exists) { + // Get the metadata. We only use the persisted flag. + QM_TRY_INSPECT(const auto& metadata, + aQuotaManager.GetDirectoryMetadata2WithRestore( + directory, + /* aPersistent */ false)); + + mPersisted = metadata.mPersisted; + } else { + // The directory has not been created yet. + mPersisted = false; + } + + return NS_OK; +} + +void PersistedOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + PersistedResponse persistedResponse; + persistedResponse.persisted() = mPersisted; + + aResponse = persistedResponse; +} + +PersistOp::PersistOp(const RequestParams& aParams) + : PersistRequestBase(aParams.get_PersistParams().principalInfo()) { + MOZ_ASSERT(aParams.type() == RequestParams::TPersistParams); +} + +nsresult PersistOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + aQuotaManager.AssertStorageIsInitialized(); + MOZ_ASSERT(!mPersistenceType.IsNull()); + MOZ_ASSERT(mPersistenceType.Value() == PERSISTENCE_TYPE_DEFAULT); + MOZ_ASSERT(mOriginScope.IsOrigin()); + + const QuotaInfo quotaInfo = {mSuffix, mGroup, + nsCString{mOriginScope.GetOrigin()}}; + + AUTO_PROFILER_LABEL("PersistOp::DoDirectoryWork", OTHER); + + // Update directory metadata on disk first. Then, create/update the originInfo + // if needed. + QM_TRY_INSPECT(const auto& directory, + aQuotaManager.GetDirectoryForOrigin(mPersistenceType.Value(), + quotaInfo.mOrigin)); + + QM_TRY_INSPECT(const bool& created, + aQuotaManager.EnsureOriginDirectory(*directory)); + + if (created) { + int64_t timestamp; + + // Origin directory has been successfully created. + // Create OriginInfo too if temporary storage was already initialized. + if (aQuotaManager.IsTemporaryStorageInitialized()) { + aQuotaManager.NoteOriginDirectoryCreated( + mPersistenceType.Value(), quotaInfo, + /* aPersisted */ true, timestamp); + } else { + timestamp = PR_Now(); + } + + QM_TRY(CreateDirectoryMetadata2(*directory, timestamp, + /* aPersisted */ true, quotaInfo)); + } else { + // Get the metadata (restore the metadata file if necessary). We only use + // the persisted flag. + QM_TRY_INSPECT(const auto& metadata, + aQuotaManager.GetDirectoryMetadata2WithRestore( + directory, + /* aPersistent */ false)); + + if (!metadata.mPersisted) { + QM_TRY_INSPECT(const auto& file, + CloneFileAndAppend( + *directory, nsLiteralString(METADATA_V2_FILE_NAME))); + + QM_TRY_INSPECT(const auto& stream, + GetBinaryOutputStream(*file, kUpdateFileFlag)); + + MOZ_ASSERT(stream); + + // Update origin access time while we are here. + QM_TRY(stream->Write64(PR_Now())); + + // Set the persisted flag to true. + QM_TRY(stream->WriteBoolean(true)); + } + + // Directory metadata has been successfully updated. + // Update OriginInfo too if temporary storage was already initialized. + if (aQuotaManager.IsTemporaryStorageInitialized()) { + aQuotaManager.PersistOrigin(quotaInfo); + } + } + + return NS_OK; +} + +void PersistOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + aResponse = PersistResponse(); +} + +EstimateOp::EstimateOp(const RequestParams& aParams) + : QuotaRequestBase(/* aExclusive */ false), mUsage(0), mLimit(0) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aParams.type() == RequestParams::TEstimateParams); + + // XXX We don't use the quota info components other than the group here. + mGroup = std::move(QuotaManager::GetInfoFromValidatedPrincipalInfo( + aParams.get_EstimateParams().principalInfo()) + .mGroup); + + // Overwrite NormalOriginOperationBase default values. + mNeedsDirectoryLocking = false; + + // Overwrite OriginOperationBase default values. + mNeedsQuotaManagerInit = true; + mNeedsStorageInit = true; +} + +nsresult EstimateOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + aQuotaManager.AssertStorageIsInitialized(); + + AUTO_PROFILER_LABEL("EstimateOp::DoDirectoryWork", OTHER); + + // Ensure temporary storage is initialized. If temporary storage hasn't been + // initialized yet, the method will initialize it by traversing the + // repositories for temporary and default storage (including origins belonging + // to our group). + QM_TRY(aQuotaManager.EnsureTemporaryStorageIsInitialized()); + + // Get cached usage (the method doesn't have to stat any files). + mUsage = aQuotaManager.GetGroupUsage(mGroup); + + mLimit = aQuotaManager.GetGroupLimit(); + + return NS_OK; +} + +void EstimateOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + EstimateResponse estimateResponse; + + estimateResponse.usage() = mUsage; + estimateResponse.limit() = mLimit; + + aResponse = estimateResponse; +} + +ListOriginsOp::ListOriginsOp() + : QuotaRequestBase(/* aExclusive */ false), TraverseRepositoryHelper() { + AssertIsOnOwningThread(); +} + +void ListOriginsOp::Init(Quota& aQuota) { + AssertIsOnOwningThread(); + + mNeedsQuotaManagerInit = true; + mNeedsStorageInit = true; +} + +nsresult ListOriginsOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + aQuotaManager.AssertStorageIsInitialized(); + + AUTO_PROFILER_LABEL("ListOriginsOp::DoDirectoryWork", OTHER); + + for (const PersistenceType type : kAllPersistenceTypes) { + QM_TRY(TraverseRepository(aQuotaManager, type)); + } + + // TraverseRepository above only consulted the file-system to get a list of + // known origins, but we also need to include origins that have pending quota + // usage. + + aQuotaManager.CollectPendingOriginsForListing([this](const auto& originInfo) { + mOrigins.AppendElement(originInfo->Origin()); + }); + + return NS_OK; +} + +const Atomic<bool>& ListOriginsOp::GetIsCanceledFlag() { + AssertIsOnIOThread(); + + return mCanceled; +} + +nsresult ListOriginsOp::ProcessOrigin(QuotaManager& aQuotaManager, + nsIFile& aOriginDir, + const bool aPersistent, + const PersistenceType aPersistenceType) { + AssertIsOnIOThread(); + + // XXX We only use metadata.mQuotaInfo.mOrigin... + QM_TRY_UNWRAP(auto metadata, + aQuotaManager.GetDirectoryMetadataWithQuotaInfo2WithRestore( + &aOriginDir, aPersistent)); + + if (aQuotaManager.IsOriginInternal(metadata.mQuotaInfo.mOrigin)) { + return NS_OK; + } + + mOrigins.AppendElement(std::move(metadata.mQuotaInfo.mOrigin)); + + return NS_OK; +} + +void ListOriginsOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + aResponse = ListOriginsResponse(); + if (mOrigins.IsEmpty()) { + return; + } + + nsTArray<nsCString>& origins = aResponse.get_ListOriginsResponse().origins(); + mOrigins.SwapElements(origins); +} + +#ifdef QM_PRINCIPALINFO_VERIFICATION_ENABLED + +// static +already_AddRefed<PrincipalVerifier> PrincipalVerifier::CreateAndDispatch( + nsTArray<PrincipalInfo>&& aPrincipalInfos) { + AssertIsOnIOThread(); + + RefPtr<PrincipalVerifier> verifier = + new PrincipalVerifier(std::move(aPrincipalInfos)); + + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(verifier)); + + return verifier.forget(); +} + +Result<Ok, nsCString> PrincipalVerifier::CheckPrincipalInfoValidity( + const PrincipalInfo& aPrincipalInfo) { + MOZ_ASSERT(NS_IsMainThread()); + + switch (aPrincipalInfo.type()) { + // A system principal is acceptable. + case PrincipalInfo::TSystemPrincipalInfo: { + return Ok{}; + } + + case PrincipalInfo::TContentPrincipalInfo: { + const ContentPrincipalInfo& info = + aPrincipalInfo.get_ContentPrincipalInfo(); + + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), info.spec()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return Err("NS_NewURI failed"_ns); + } + + nsCOMPtr<nsIPrincipal> principal = + BasePrincipal::CreateContentPrincipal(uri, info.attrs()); + if (NS_WARN_IF(!principal)) { + return Err("CreateContentPrincipal failed"_ns); + } + + nsCString originNoSuffix; + rv = principal->GetOriginNoSuffix(originNoSuffix); + if (NS_WARN_IF(NS_FAILED(rv))) { + return Err("GetOriginNoSuffix failed"_ns); + } + + if (NS_WARN_IF(originNoSuffix != info.originNoSuffix())) { + static const char messageTemplate[] = + "originNoSuffix (%s) doesn't match passed one (%s)!"; + + QM_WARNING(messageTemplate, originNoSuffix.get(), + info.originNoSuffix().get()); + + return Err(nsPrintfCString( + messageTemplate, AnonymizedOriginString(originNoSuffix).get(), + AnonymizedOriginString(info.originNoSuffix()).get())); + } + + nsCString baseDomain; + rv = principal->GetBaseDomain(baseDomain); + if (NS_WARN_IF(NS_FAILED(rv))) { + return Err("GetBaseDomain failed"_ns); + } + + if (NS_WARN_IF(baseDomain != info.baseDomain())) { + static const char messageTemplate[] = + "baseDomain (%s) doesn't match passed one (%s)!"; + + QM_WARNING(messageTemplate, baseDomain.get(), info.baseDomain().get()); + + return Err(nsPrintfCString(messageTemplate, + AnonymizedCString(baseDomain).get(), + AnonymizedCString(info.baseDomain()).get())); + } + + return Ok{}; + } + + default: { + break; + } + } + + return Err("Null and expanded principals are not acceptable"_ns); +} + +NS_IMETHODIMP +PrincipalVerifier::Run() { + MOZ_ASSERT(NS_IsMainThread()); + + nsAutoCString allDetails; + for (auto& principalInfo : mPrincipalInfos) { + const auto res = CheckPrincipalInfoValidity(principalInfo); + if (res.isErr()) { + if (!allDetails.IsEmpty()) { + allDetails.AppendLiteral(", "); + } + + allDetails.Append(res.inspectErr()); + } + } + + if (!allDetails.IsEmpty()) { + allDetails.Insert("Invalid principal infos found: ", 0); + + // In case of invalid principal infos, this will produce a crash reason such + // as: + // Invalid principal infos found: originNoSuffix (https://aaa.aaaaaaa.aaa) + // doesn't match passed one (about:aaaa)! + // + // In case of errors while validating a principal, it will contain a + // different message describing that error, which does not contain any + // details of the actual principal info at the moment. + // + // This string will be leaked. + MOZ_CRASH_UNSAFE(strdup(allDetails.BeginReading())); + } + + return NS_OK; +} + +#endif + +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(binaryStream, Read64)); + + QM_TRY_INSPECT(const auto& group, MOZ_TO_RESULT_INVOKE_TYPED( + nsCString, binaryStream, ReadCString)); + + QM_TRY_INSPECT(const auto& origin, MOZ_TO_RESULT_INVOKE_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(binaryStream, Read64)); + + QM_TRY_INSPECT(const bool& persisted, + MOZ_TO_RESULT_INVOKE(binaryStream, ReadBoolean)); + Unused << persisted; + + QM_TRY_INSPECT(const bool& reservedData1, + MOZ_TO_RESULT_INVOKE(binaryStream, Read32)); + Unused << reservedData1; + + QM_TRY_INSPECT(const bool& reservedData2, + MOZ_TO_RESULT_INVOKE(binaryStream, Read32)); + Unused << reservedData2; + + QM_TRY_INSPECT(const auto& suffix, MOZ_TO_RESULT_INVOKE_TYPED( + nsCString, binaryStream, ReadCString)); + + QM_TRY_INSPECT(const auto& group, MOZ_TO_RESULT_INVOKE_TYPED( + nsCString, binaryStream, ReadCString)); + + QM_TRY_INSPECT(const auto& origin, MOZ_TO_RESULT_INVOKE_TYPED( + nsCString, binaryStream, ReadCString)); + + QM_TRY_INSPECT(const bool& isApp, + MOZ_TO_RESULT_INVOKE(binaryStream, ReadBoolean)); + + aTimestamp = timestamp; + aSuffix = suffix; + aGroup = group; + aOrigin = origin; + aIsApp = isApp; + return NS_OK; +} + +nsresult StorageOperationBase::RemoveObsoleteOrigin( + const OriginProps& aOriginProps) { + AssertIsOnIOThread(); + MOZ_ASSERT(aOriginProps.mDirectory); + + QM_WARNING( + "Deleting obsolete %s directory that is no longer a legal " + "origin!", + NS_ConvertUTF16toUTF8(aOriginProps.mLeafName).get()); + + QM_TRY(aOriginProps.mDirectory->Remove(/* recursive */ true)); + + return NS_OK; +} + +nsresult StorageOperationBase::ProcessOriginDirectories() { + AssertIsOnIOThread(); + MOZ_ASSERT(!mOriginProps.IsEmpty()); + +#ifdef QM_PRINCIPALINFO_VERIFICATION_ENABLED + nsTArray<PrincipalInfo> principalInfos; +#endif + + for (auto& originProps : mOriginProps) { + switch (originProps.mType) { + case OriginProps::eChrome: { + originProps.mQuotaInfo = QuotaManager::GetInfoForChrome(); + break; + } + + case OriginProps::eContent: { + RefPtr<MozURL> specURL; + nsresult rv = MozURL::Init(getter_AddRefs(specURL), originProps.mSpec); + if (NS_WARN_IF(NS_FAILED(rv))) { + // If a URL cannot be understood by MozURL during restoring or + // upgrading, either marking the directory as broken or removing that + // corresponding directory should be considered. While the cost of + // marking the directory as broken during a upgrade is too high, + // removing the directory is a better choice rather than blocking the + // initialization or the upgrade. + QM_WARNING( + "A URL (%s) for the origin directory is not recognized by " + "MozURL. The directory will be deleted for now to pass the " + "initialization or the upgrade.", + originProps.mSpec.get()); + + originProps.mType = OriginProps::eObsolete; + break; + } + + nsCString originNoSuffix; + specURL->Origin(originNoSuffix); + + QM_TRY_INSPECT( + const auto& baseDomain, + MOZ_TO_RESULT_INVOKE_TYPED(nsCString, specURL, BaseDomain)); + + ContentPrincipalInfo contentPrincipalInfo; + contentPrincipalInfo.attrs() = originProps.mAttrs; + contentPrincipalInfo.originNoSuffix() = originNoSuffix; + contentPrincipalInfo.spec() = originProps.mSpec; + contentPrincipalInfo.baseDomain() = baseDomain; + + PrincipalInfo principalInfo(contentPrincipalInfo); + + originProps.mQuotaInfo = + QuotaManager::GetInfoFromValidatedPrincipalInfo(principalInfo); + +#ifdef QM_PRINCIPALINFO_VERIFICATION_ENABLED + principalInfos.AppendElement(principalInfo); +#endif + + break; + } + + case OriginProps::eObsolete: { + // There's no way to get info for obsolete origins. + break; + } + + default: + MOZ_CRASH("Bad type!"); + } + } + +#ifdef QM_PRINCIPALINFO_VERIFICATION_ENABLED + if (!principalInfos.IsEmpty()) { + RefPtr<PrincipalVerifier> principalVerifier = + PrincipalVerifier::CreateAndDispatch(std::move(principalInfos)); + } +#endif + + // 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.mQuotaInfo.mSuffix.IsEmpty()); + MOZ_ASSERT(originProps.mQuotaInfo.mGroup.IsEmpty()); + MOZ_ASSERT(originProps.mQuotaInfo.mOrigin.IsEmpty()); + + QM_TRY(RemoveObsoleteOrigin(originProps)); + } else { + MOZ_ASSERT(!originProps.mQuotaInfo.mGroup.IsEmpty()); + MOZ_ASSERT(!originProps.mQuotaInfo.mOrigin.IsEmpty()); + + QM_TRY(ProcessOriginDirectory(originProps)); + } + } + + return NS_OK; +} + +nsresult StorageOperationBase::OriginProps::Init(nsIFile* aDirectory) { + AssertIsOnIOThread(); + MOZ_ASSERT(aDirectory); + + QM_TRY_INSPECT( + const auto& leafName, + MOZ_TO_RESULT_INVOKE_TYPED(nsAutoString, aDirectory, GetLeafName)); + + nsCString spec; + OriginAttributes attrs; + nsCString originalSuffix; + OriginParser::ResultType result = OriginParser::ParseOrigin( + NS_ConvertUTF16toUTF8(leafName), spec, &attrs, originalSuffix); + if (NS_WARN_IF(result == OriginParser::InvalidOrigin)) { + mType = OriginProps::eInvalid; + return NS_OK; + } + + mDirectory = aDirectory; + mLeafName = leafName; + mSpec = spec; + mAttrs = attrs; + mOriginalSuffix = originalSuffix; + if (result == OriginParser::ObsoleteOrigin) { + mType = eObsolete; + } else if (mSpec.EqualsLiteral(kChromeOrigin)) { + mType = eChrome; + } else { + mType = eContent; + } + + return NS_OK; +} + +// static +auto OriginParser::ParseOrigin(const nsACString& aOrigin, nsCString& aSpec, + OriginAttributes* aAttrs, + nsCString& aOriginalSuffix) -> ResultType { + MOZ_ASSERT(!aOrigin.IsEmpty()); + MOZ_ASSERT(aAttrs); + + nsCString origin(aOrigin); + int32_t pos = origin.RFindChar('^'); + + if (pos == kNotFound) { + aOriginalSuffix.Truncate(); + } else { + aOriginalSuffix = Substring(origin, pos); + } + + OriginAttributes originAttributes; + + nsCString originNoSuffix; + bool ok = originAttributes.PopulateFromOrigin(aOrigin, originNoSuffix); + if (!ok) { + return InvalidOrigin; + } + + OriginParser parser(originNoSuffix); + + *aAttrs = originAttributes; + return parser.Parse(aSpec); +} + +auto OriginParser::Parse(nsACString& aSpec) -> ResultType { + while (mTokenizer.hasMoreTokens()) { + const nsDependentCSubstring& token = mTokenizer.nextToken(); + + HandleToken(token); + + if (mError) { + break; + } + + if (!mHandledTokens.IsEmpty()) { + mHandledTokens.AppendLiteral(", "); + } + mHandledTokens.Append('\''); + mHandledTokens.Append(token); + mHandledTokens.Append('\''); + } + + if (!mError && mTokenizer.separatorAfterCurrentToken()) { + HandleTrailingSeparator(); + } + + if (mError) { + QM_WARNING("Origin '%s' failed to parse, handled tokens: %s", mOrigin.get(), + mHandledTokens.get()); + + return (mSchemeType == eChrome || mSchemeType == eAbout) ? ObsoleteOrigin + : InvalidOrigin; + } + + MOZ_ASSERT(mState == eComplete || mState == eHandledTrailingSeparator); + + // For IPv6 URL, it should at least have three groups. + MOZ_ASSERT_IF(mIPGroup > 0, mIPGroup >= 3); + + nsAutoCString spec(mScheme); + + if (mSchemeType == eFile) { + spec.AppendLiteral("://"); + + if (mUniversalFileOrigin) { + MOZ_ASSERT(mPathnameComponents.Length() == 1); + + spec.Append(mPathnameComponents[0]); + } else { + for (uint32_t count = mPathnameComponents.Length(), index = 0; + index < count; index++) { + spec.Append('/'); + spec.Append(mPathnameComponents[index]); + } + } + + aSpec = spec; + + return ValidOrigin; + } + + if (mSchemeType == eAbout) { + if (mMaybeObsolete) { + // The "moz-safe-about+++home" was acciedntally created by a buggy nightly + // and can be safely removed. + return mHost.EqualsLiteral("home") ? ObsoleteOrigin : InvalidOrigin; + } + spec.Append(':'); + } else if (mSchemeType != eChrome) { + spec.AppendLiteral("://"); + } + + spec.Append(mHost); + + if (!mPort.IsNull()) { + spec.Append(':'); + spec.AppendInt(mPort.Value()); + } + + aSpec = spec; + + return mScheme.EqualsLiteral("app") ? ObsoleteOrigin : ValidOrigin; +} + +void OriginParser::HandleScheme(const nsDependentCSubstring& aToken) { + MOZ_ASSERT(!aToken.IsEmpty()); + MOZ_ASSERT(mState == eExpectingAppIdOrScheme || mState == eExpectingScheme); + + bool isAbout = false; + bool isMozSafeAbout = false; + bool isFile = false; + bool isChrome = false; + if (aToken.EqualsLiteral("http") || aToken.EqualsLiteral("https") || + (isAbout = aToken.EqualsLiteral("about") || + (isMozSafeAbout = aToken.EqualsLiteral("moz-safe-about"))) || + aToken.EqualsLiteral("indexeddb") || + (isFile = aToken.EqualsLiteral("file")) || aToken.EqualsLiteral("app") || + aToken.EqualsLiteral("resource") || + aToken.EqualsLiteral("moz-extension") || + (isChrome = aToken.EqualsLiteral(kChromeOrigin))) { + mScheme = aToken; + + if (isAbout) { + mSchemeType = eAbout; + mState = isMozSafeAbout ? eExpectingEmptyToken1OrHost : eExpectingHost; + } else if (isChrome) { + mSchemeType = eChrome; + if (mTokenizer.hasMoreTokens()) { + mError = true; + } + mState = eComplete; + } else { + if (isFile) { + mSchemeType = eFile; + } + mState = eExpectingEmptyToken1; + } + + return; + } + + QM_WARNING("'%s' is not a valid scheme!", nsCString(aToken).get()); + + mError = true; +} + +void OriginParser::HandlePathnameComponent( + const nsDependentCSubstring& aToken) { + MOZ_ASSERT(!aToken.IsEmpty()); + MOZ_ASSERT(mState == eExpectingEmptyTokenOrDriveLetterOrPathnameComponent || + mState == eExpectingEmptyTokenOrPathnameComponent); + MOZ_ASSERT(mSchemeType == eFile); + + mPathnameComponents.AppendElement(aToken); + + mState = mTokenizer.hasMoreTokens() ? eExpectingEmptyTokenOrPathnameComponent + : eComplete; +} + +void OriginParser::HandleToken(const nsDependentCSubstring& aToken) { + switch (mState) { + case eExpectingAppIdOrScheme: { + if (aToken.IsEmpty()) { + QM_WARNING("Expected an app id or scheme (not an empty string)!"); + + mError = true; + return; + } + + if (IsAsciiDigit(aToken.First())) { + // nsDependentCSubstring doesn't provice ToInteger() + nsCString token(aToken); + + nsresult rv; + Unused << token.ToInteger(&rv); + if (NS_SUCCEEDED(rv)) { + mState = eExpectingInMozBrowser; + return; + } + } + + HandleScheme(aToken); + + return; + } + + case eExpectingInMozBrowser: { + if (aToken.Length() != 1) { + QM_WARNING("'%d' is not a valid length for the inMozBrowser flag!", + aToken.Length()); + + mError = true; + return; + } + + if (aToken.First() == 't') { + mInIsolatedMozBrowser = true; + } else if (aToken.First() == 'f') { + mInIsolatedMozBrowser = false; + } else { + QM_WARNING("'%s' is not a valid value for the inMozBrowser flag!", + nsCString(aToken).get()); + + mError = true; + return; + } + + mState = eExpectingScheme; + + return; + } + + case eExpectingScheme: { + if (aToken.IsEmpty()) { + QM_WARNING("Expected a scheme (not an empty string)!"); + + mError = true; + return; + } + + HandleScheme(aToken); + + return; + } + + case eExpectingEmptyToken1: { + if (!aToken.IsEmpty()) { + QM_WARNING("Expected the first empty token!"); + + mError = true; + return; + } + + mState = eExpectingEmptyToken2; + + return; + } + + case eExpectingEmptyToken2: { + if (!aToken.IsEmpty()) { + QM_WARNING("Expected the second empty token!"); + + mError = true; + return; + } + + if (mSchemeType == eFile) { + mState = eExpectingEmptyTokenOrUniversalFileOrigin; + } else { + if (mSchemeType == eAbout) { + mMaybeObsolete = true; + } + mState = eExpectingHost; + } + + return; + } + + case eExpectingEmptyTokenOrUniversalFileOrigin: { + MOZ_ASSERT(mSchemeType == eFile); + + if (aToken.IsEmpty()) { + mState = mTokenizer.hasMoreTokens() + ? eExpectingEmptyTokenOrDriveLetterOrPathnameComponent + : eComplete; + + return; + } + + if (aToken.EqualsLiteral("UNIVERSAL_FILE_URI_ORIGIN")) { + mUniversalFileOrigin = true; + + mPathnameComponents.AppendElement(aToken); + + mState = eComplete; + + return; + } + + QM_WARNING( + "Expected the third empty token or " + "UNIVERSAL_FILE_URI_ORIGIN!"); + + mError = true; + return; + } + + case eExpectingHost: { + if (aToken.IsEmpty()) { + QM_WARNING("Expected a host (not an empty string)!"); + + mError = true; + return; + } + + mHost = aToken; + + if (aToken.First() == '[') { + MOZ_ASSERT(mIPGroup == 0); + + ++mIPGroup; + mState = eExpectingIPV6Token; + + MOZ_ASSERT(mTokenizer.hasMoreTokens()); + return; + } + + if (mTokenizer.hasMoreTokens()) { + if (mSchemeType == eAbout) { + QM_WARNING("Expected an empty string after host!"); + + mError = true; + return; + } + + mState = eExpectingPort; + + return; + } + + mState = eComplete; + + return; + } + + case eExpectingPort: { + MOZ_ASSERT(mSchemeType == eNone); + + if (aToken.IsEmpty()) { + QM_WARNING("Expected a port (not an empty string)!"); + + mError = true; + return; + } + + // nsDependentCSubstring doesn't provice ToInteger() + nsCString token(aToken); + + nsresult rv; + uint32_t port = token.ToInteger(&rv); + if (NS_SUCCEEDED(rv)) { + mPort.SetValue() = port; + } else { + QM_WARNING("'%s' is not a valid port number!", token.get()); + + mError = true; + return; + } + + mState = eComplete; + + return; + } + + case eExpectingEmptyTokenOrDriveLetterOrPathnameComponent: { + MOZ_ASSERT(mSchemeType == eFile); + + if (aToken.IsEmpty()) { + mPathnameComponents.AppendElement(""_ns); + + mState = mTokenizer.hasMoreTokens() + ? eExpectingEmptyTokenOrPathnameComponent + : eComplete; + + return; + } + + if (aToken.Length() == 1 && IsAsciiAlpha(aToken.First())) { + mMaybeDriveLetter = true; + + mPathnameComponents.AppendElement(aToken); + + mState = mTokenizer.hasMoreTokens() + ? eExpectingEmptyTokenOrPathnameComponent + : eComplete; + + return; + } + + HandlePathnameComponent(aToken); + + return; + } + + case eExpectingEmptyTokenOrPathnameComponent: { + MOZ_ASSERT(mSchemeType == eFile); + + if (aToken.IsEmpty()) { + if (mMaybeDriveLetter) { + MOZ_ASSERT(mPathnameComponents.Length() == 1); + + nsCString& pathnameComponent = mPathnameComponents[0]; + pathnameComponent.Append(':'); + + mMaybeDriveLetter = false; + } else { + mPathnameComponents.AppendElement(""_ns); + } + + mState = mTokenizer.hasMoreTokens() + ? eExpectingEmptyTokenOrPathnameComponent + : eComplete; + + return; + } + + HandlePathnameComponent(aToken); + + return; + } + + case eExpectingEmptyToken1OrHost: { + MOZ_ASSERT(mSchemeType == eAbout && + mScheme.EqualsLiteral("moz-safe-about")); + + if (aToken.IsEmpty()) { + mState = eExpectingEmptyToken2; + } else { + mHost = aToken; + mState = mTokenizer.hasMoreTokens() ? eExpectingPort : eComplete; + } + + return; + } + + case eExpectingIPV6Token: { + // A safe check for preventing infinity recursion. + if (++mIPGroup > 8) { + mError = true; + return; + } + + mHost.AppendLiteral(":"); + mHost.Append(aToken); + if (!aToken.IsEmpty() && aToken.Last() == ']') { + mState = mTokenizer.hasMoreTokens() ? eExpectingPort : eComplete; + } + + return; + } + + default: + MOZ_CRASH("Should never get here!"); + } +} + +void OriginParser::HandleTrailingSeparator() { + MOZ_ASSERT(mState == eComplete); + MOZ_ASSERT(mSchemeType == eFile); + + mPathnameComponents.AppendElement(""_ns); + + mState = eHandledTrailingSeparator; +} + +nsresult RepositoryOperationBase::ProcessRepository() { + AssertIsOnIOThread(); + +#ifdef DEBUG + { + QM_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE(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_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; + QM_TRY(originProps.Init(originDir)); + // 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(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(ProcessOriginDirectories()); + + return NS_OK; +} + +template <typename UpgradeMethod> +nsresult RepositoryOperationBase::MaybeUpgradeClients( + const OriginProps& aOriginProps, UpgradeMethod aMethod) { + AssertIsOnIOThread(); + MOZ_ASSERT(aOriginProps.mDirectory); + 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_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_TYPED(nsAutoString, dir, GetLeafName)); + + QM_TRY_INSPECT( + const bool& removed, + MOZ_TO_RESULT_INVOKE(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((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::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(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)); + + QM_TRY( + ToResult(idbDirectory->Create(nsIFile::DIRECTORY_TYPE, 0755)) + .orElse([&idbDirectory](const nsresult rv) -> Result<Ok, nsresult> { + if (rv == NS_ERROR_FILE_ALREADY_EXISTS) { + NS_WARNING("IDB directory already exists!"); + + QM_TRY_INSPECT(const bool& isDirectory, + MOZ_TO_RESULT_INVOKE(idbDirectory, IsDirectory)); + + QM_TRY(OkIf(isDirectory), Err(NS_ERROR_UNEXPECTED)); + + return Ok{}; + } + + return Err(rv); + })); + + QM_TRY(CollectEachFile( + *aDirectory, + [&idbDirectory, &idbDirectoryName]( + const nsCOMPtr<nsIFile>& file) -> Result<Ok, nsresult> { + QM_TRY_INSPECT( + const auto& leafName, + MOZ_TO_RESULT_INVOKE_TYPED(nsAutoString, file, GetLeafName)); + + if (!leafName.Equals(idbDirectoryName)) { + QM_TRY(file->MoveTo(idbDirectory, u""_ns)); + } + + return Ok{}; + })); + + QM_TRY(metadataFile->Create(nsIFile::NORMAL_FILE_TYPE, 0644)); + } + + return NS_OK; +} + +nsresult CreateOrUpgradeDirectoryMetadataHelper::PrepareOriginDirectory( + OriginProps& aOriginProps, bool* aRemoved) { + AssertIsOnIOThread(); + MOZ_ASSERT(aOriginProps.mDirectory); + MOZ_ASSERT(aRemoved); + + if (mPersistent) { + QM_TRY(MaybeUpgradeOriginDirectory(aOriginProps.mDirectory)); + + const bool persistent = QuotaManager::IsOriginInternal(aOriginProps.mSpec); + aOriginProps.mTimestamp = + GetLastModifiedTime(aOriginProps.mDirectory, persistent); + } else { + int64_t timestamp; + nsCString group; + nsCString origin; + Nullable<bool> isApp; + nsresult rv = GetDirectoryMetadata(aOriginProps.mDirectory, timestamp, + group, origin, isApp); + if (NS_FAILED(rv)) { + aOriginProps.mTimestamp = + GetLastModifiedTime(aOriginProps.mDirectory, mPersistent); + aOriginProps.mNeedsRestore = true; + } else if (!isApp.IsNull()) { + aOriginProps.mIgnore = true; + } + } + + *aRemoved = false; + return NS_OK; +} + +nsresult CreateOrUpgradeDirectoryMetadataHelper::ProcessOriginDirectory( + const OriginProps& aOriginProps) { + AssertIsOnIOThread(); + + if (mPersistent) { + QM_TRY(CreateDirectoryMetadata(*aOriginProps.mDirectory, + aOriginProps.mTimestamp, + aOriginProps.mQuotaInfo)); + + // Move internal origins to new persistent storage. + if (QuotaManager::IsOriginInternal(aOriginProps.mSpec)) { + if (!mPermanentStorageDir) { + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + const nsString& permanentStoragePath = + quotaManager->GetStoragePath(PERSISTENCE_TYPE_PERSISTENT); + + QM_TRY_UNWRAP(mPermanentStorageDir, + QM_NewLocalFile(permanentStoragePath)); + } + + QM_TRY_INSPECT(const auto& leafName, + MOZ_TO_RESULT_INVOKE_TYPED( + nsAutoString, aOriginProps.mDirectory, GetLeafName)); + + QM_TRY_INSPECT(const auto& newDirectory, + CloneFileAndAppend(*mPermanentStorageDir, leafName)); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE(newDirectory, Exists)); + + if (exists) { + QM_WARNING("Found %s in storage/persistent and storage/permanent !", + NS_ConvertUTF16toUTF8(leafName).get()); + + QM_TRY(aOriginProps.mDirectory->Remove(/* recursive */ true)); + } else { + QM_TRY(aOriginProps.mDirectory->MoveTo(mPermanentStorageDir, u""_ns)); + } + } + } else if (aOriginProps.mNeedsRestore) { + QM_TRY(CreateDirectoryMetadata(*aOriginProps.mDirectory, + aOriginProps.mTimestamp, + aOriginProps.mQuotaInfo)); + } 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, kAppendFileFlag)); + + MOZ_ASSERT(stream); + + // Currently unused (used to be isApp). + QM_TRY(stream->WriteBoolean(false)); + } + + return NS_OK; +} + +nsresult UpgradeStorageFrom0_0To1_0Helper::PrepareOriginDirectory( + OriginProps& aOriginProps, bool* aRemoved) { + AssertIsOnIOThread(); + MOZ_ASSERT(aOriginProps.mDirectory); + MOZ_ASSERT(aRemoved); + + int64_t timestamp; + nsCString group; + nsCString origin; + Nullable<bool> isApp; + nsresult rv = GetDirectoryMetadata(aOriginProps.mDirectory, timestamp, group, + origin, isApp); + if (NS_FAILED(rv) || isApp.IsNull()) { + aOriginProps.mTimestamp = + GetLastModifiedTime(aOriginProps.mDirectory, mPersistent); + aOriginProps.mNeedsRestore = true; + } else { + aOriginProps.mTimestamp = timestamp; + } + + *aRemoved = false; + return NS_OK; +} + +nsresult UpgradeStorageFrom0_0To1_0Helper::ProcessOriginDirectory( + const OriginProps& aOriginProps) { + AssertIsOnIOThread(); + + if (aOriginProps.mNeedsRestore) { + QM_TRY(CreateDirectoryMetadata(*aOriginProps.mDirectory, + aOriginProps.mTimestamp, + aOriginProps.mQuotaInfo)); + } + + QM_TRY(CreateDirectoryMetadata2( + *aOriginProps.mDirectory, aOriginProps.mTimestamp, + /* aPersisted */ false, aOriginProps.mQuotaInfo)); + + QM_TRY_INSPECT(const auto& oldName, + MOZ_TO_RESULT_INVOKE_TYPED( + nsAutoString, aOriginProps.mDirectory, GetLeafName)); + + const auto newName = + MakeSanitizedOriginString(aOriginProps.mQuotaInfo.mOrigin); + + if (!oldName.Equals(newName)) { + QM_TRY(aOriginProps.mDirectory->RenameTo(nullptr, newName)); + } + + return NS_OK; +} + +nsresult UpgradeStorageFrom1_0To2_0Helper::MaybeRemoveMorgueDirectory( + const OriginProps& aOriginProps) { + AssertIsOnIOThread(); + MOZ_ASSERT(aOriginProps.mDirectory); + + // 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_TYPED(nsCOMPtr<nsIFile>, + aOriginProps.mDirectory, Clone)); + + QM_TRY(morgueDir->Append(u"morgue"_ns)); + + QM_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE(morgueDir, Exists)); + + if (exists) { + QM_WARNING("Deleting accidental morgue directory!"); + + QM_TRY(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(RemoveObsoleteOrigin(aOriginProps)); + + return true; + } + } + + return false; +} + +Result<bool, nsresult> +UpgradeStorageFrom1_0To2_0Helper::MaybeStripObsoleteOriginAttributes( + const OriginProps& aOriginProps) { + AssertIsOnIOThread(); + MOZ_ASSERT(aOriginProps.mDirectory); + + const nsAString& oldLeafName = aOriginProps.mLeafName; + + const auto newLeafName = + MakeSanitizedOriginString(aOriginProps.mQuotaInfo.mOrigin); + + if (oldLeafName == newLeafName) { + return false; + } + + QM_TRY(CreateDirectoryMetadata(*aOriginProps.mDirectory, + aOriginProps.mTimestamp, + aOriginProps.mQuotaInfo)); + + QM_TRY(CreateDirectoryMetadata2( + *aOriginProps.mDirectory, aOriginProps.mTimestamp, + /* aPersisted */ false, aOriginProps.mQuotaInfo)); + + QM_TRY_INSPECT(const auto& newFile, + MOZ_TO_RESULT_INVOKE_TYPED( + nsCOMPtr<nsIFile>, aOriginProps.mDirectory, GetParent)); + + QM_TRY(newFile->Append(newLeafName)); + + QM_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE(newFile, Exists)); + + if (exists) { + QM_WARNING( + "Can't rename %s directory, %s directory already exists, " + "removing!", + NS_ConvertUTF16toUTF8(oldLeafName).get(), + NS_ConvertUTF16toUTF8(newLeafName).get()); + + QM_TRY(aOriginProps.mDirectory->Remove(/* recursive */ true)); + } else { + QM_TRY(aOriginProps.mDirectory->RenameTo(nullptr, newLeafName)); + } + + return true; +} + +nsresult UpgradeStorageFrom1_0To2_0Helper::PrepareOriginDirectory( + OriginProps& aOriginProps, bool* aRemoved) { + AssertIsOnIOThread(); + MOZ_ASSERT(aOriginProps.mDirectory); + MOZ_ASSERT(aRemoved); + + QM_TRY(MaybeRemoveMorgueDirectory(aOriginProps)); + + QM_TRY( + 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; + nsresult rv = GetDirectoryMetadata(aOriginProps.mDirectory, timestamp, group, + origin, isApp); + if (NS_FAILED(rv) || isApp.IsNull()) { + aOriginProps.mNeedsRestore = true; + } + + nsCString suffix; + rv = GetDirectoryMetadata2(aOriginProps.mDirectory, timestamp, suffix, group, + origin, isApp.SetValue()); + if (NS_FAILED(rv)) { + aOriginProps.mTimestamp = + GetLastModifiedTime(aOriginProps.mDirectory, mPersistent); + aOriginProps.mNeedsRestore2 = true; + } else { + aOriginProps.mTimestamp = timestamp; + } + + *aRemoved = false; + return NS_OK; +} + +nsresult UpgradeStorageFrom1_0To2_0Helper::ProcessOriginDirectory( + const OriginProps& aOriginProps) { + AssertIsOnIOThread(); + + QM_TRY_INSPECT(const bool& stripped, + MaybeStripObsoleteOriginAttributes(aOriginProps)); + if (stripped) { + return NS_OK; + } + + if (aOriginProps.mNeedsRestore) { + QM_TRY(CreateDirectoryMetadata(*aOriginProps.mDirectory, + aOriginProps.mTimestamp, + aOriginProps.mQuotaInfo)); + } + + if (aOriginProps.mNeedsRestore2) { + QM_TRY(CreateDirectoryMetadata2( + *aOriginProps.mDirectory, aOriginProps.mTimestamp, + /* aPersisted */ false, aOriginProps.mQuotaInfo)); + } + + return NS_OK; +} + +nsresult UpgradeStorageFrom2_0To2_1Helper::PrepareOriginDirectory( + OriginProps& aOriginProps, bool* aRemoved) { + AssertIsOnIOThread(); + MOZ_ASSERT(aOriginProps.mDirectory); + MOZ_ASSERT(aRemoved); + + QM_TRY( + MaybeUpgradeClients(aOriginProps, &Client::UpgradeStorageFrom2_0To2_1)); + + int64_t timestamp; + nsCString group; + nsCString origin; + Nullable<bool> isApp; + nsresult rv = GetDirectoryMetadata(aOriginProps.mDirectory, timestamp, group, + origin, isApp); + if (NS_FAILED(rv) || isApp.IsNull()) { + aOriginProps.mNeedsRestore = true; + } + + nsCString suffix; + rv = GetDirectoryMetadata2(aOriginProps.mDirectory, timestamp, suffix, group, + origin, isApp.SetValue()); + if (NS_FAILED(rv)) { + aOriginProps.mTimestamp = + GetLastModifiedTime(aOriginProps.mDirectory, mPersistent); + 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(CreateDirectoryMetadata(*aOriginProps.mDirectory, + aOriginProps.mTimestamp, + aOriginProps.mQuotaInfo)); + } + + if (aOriginProps.mNeedsRestore2) { + QM_TRY(CreateDirectoryMetadata2( + *aOriginProps.mDirectory, aOriginProps.mTimestamp, + /* aPersisted */ false, aOriginProps.mQuotaInfo)); + } + + return NS_OK; +} + +nsresult UpgradeStorageFrom2_1To2_2Helper::PrepareOriginDirectory( + OriginProps& aOriginProps, bool* aRemoved) { + AssertIsOnIOThread(); + MOZ_ASSERT(aOriginProps.mDirectory); + MOZ_ASSERT(aRemoved); + + QM_TRY( + MaybeUpgradeClients(aOriginProps, &Client::UpgradeStorageFrom2_1To2_2)); + + int64_t timestamp; + nsCString group; + nsCString origin; + Nullable<bool> isApp; + nsresult rv = GetDirectoryMetadata(aOriginProps.mDirectory, timestamp, group, + origin, isApp); + if (NS_FAILED(rv) || isApp.IsNull()) { + aOriginProps.mNeedsRestore = true; + } + + nsCString suffix; + rv = GetDirectoryMetadata2(aOriginProps.mDirectory, timestamp, suffix, group, + origin, isApp.SetValue()); + if (NS_FAILED(rv)) { + aOriginProps.mTimestamp = + GetLastModifiedTime(aOriginProps.mDirectory, mPersistent); + 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(CreateDirectoryMetadata(*aOriginProps.mDirectory, + aOriginProps.mTimestamp, + aOriginProps.mQuotaInfo)); + } + + if (aOriginProps.mNeedsRestore2) { + QM_TRY(CreateDirectoryMetadata2( + *aOriginProps.mDirectory, aOriginProps.mTimestamp, + /* aPersisted */ false, aOriginProps.mQuotaInfo)); + } + + 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(aFile->Remove(true)); + + aRemoved = true; + } else { + aRemoved = false; + } + + return NS_OK; +} + +nsresult RestoreDirectoryMetadata2Helper::RestoreMetadata2File() { + AssertIsOnIOThread(); + + OriginProps originProps; + QM_TRY(originProps.Init(mDirectory)); + + QM_TRY(OkIf(originProps.mType != OriginProps::eInvalid), NS_ERROR_FAILURE); + + originProps.mTimestamp = GetLastModifiedTime(mDirectory, mPersistent); + + mOriginProps.AppendElement(std::move(originProps)); + + QM_TRY(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(CreateDirectoryMetadata2( + *aOriginProps.mDirectory, aOriginProps.mTimestamp, + /* aPersisted */ false, aOriginProps.mQuotaInfo)); + + return NS_OK; +} + +} // namespace mozilla::dom::quota |