/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "ActorsParent.h"

// Local includes
#include "CanonicalQuotaObject.h"
#include "ClientUsageArray.h"
#include "Flatten.h"
#include "FirstInitializationAttemptsImpl.h"
#include "GroupInfo.h"
#include "GroupInfoPair.h"
#include "NormalOriginOperationBase.h"
#include "OriginOperationBase.h"
#include "OriginOperations.h"
#include "OriginScope.h"
#include "OriginInfo.h"
#include "QuotaCommon.h"
#include "QuotaManager.h"
#include "ResolvableNormalOriginOp.h"
#include "SanitizationUtils.h"
#include "ScopedLogExtraInfo.h"
#include "UsageInfo.h"

// Global includes
#include <cinttypes>
#include <cstdlib>
#include <cstring>
#include <algorithm>
#include <cstdint>
#include <functional>
#include <new>
#include <numeric>
#include <tuple>
#include <type_traits>
#include <utility>
#include "DirectoryLockImpl.h"
#include "ErrorList.h"
#include "MainThreadUtils.h"
#include "mozIStorageAsyncConnection.h"
#include "mozIStorageConnection.h"
#include "mozIStorageService.h"
#include "mozIStorageStatement.h"
#include "mozStorageCID.h"
#include "mozStorageHelper.h"
#include "mozilla/AlreadyAddRefed.h"
#include "mozilla/Assertions.h"
#include "mozilla/Atomics.h"
#include "mozilla/Attributes.h"
#include "mozilla/AutoRestore.h"
#include "mozilla/BasePrincipal.h"
#include "mozilla/CheckedInt.h"
#include "mozilla/CondVar.h"
#include "mozilla/InitializedOnce.h"
#include "mozilla/Logging.h"
#include "mozilla/MacroForEach.h"
#include "mozilla/Maybe.h"
#include "mozilla/Mutex.h"
#include "mozilla/NotNull.h"
#include "mozilla/OriginAttributes.h"
#include "mozilla/Preferences.h"
#include "mozilla/RefPtr.h"
#include "mozilla/Result.h"
#include "mozilla/ResultExtensions.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/Services.h"
#include "mozilla/SpinEventLoopUntil.h"
#include "mozilla/StaticPrefs_dom.h"
#include "mozilla/StaticPtr.h"
#include "mozilla/SystemPrincipal.h"
#include "mozilla/Telemetry.h"
#include "mozilla/TelemetryHistogramEnums.h"
#include "mozilla/TextUtils.h"
#include "mozilla/TimeStamp.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/Unused.h"
#include "mozilla/Variant.h"
#include "mozilla/dom/FileSystemQuotaClient.h"
#include "mozilla/dom/FlippedOnce.h"
#include "mozilla/dom/LocalStorageCommon.h"
#include "mozilla/dom/StorageDBUpdater.h"
#include "mozilla/dom/cache/QuotaClient.h"
#include "mozilla/dom/indexedDB/ActorsParent.h"
#include "mozilla/dom/ipc/IdType.h"
#include "mozilla/dom/localstorage/ActorsParent.h"
#include "mozilla/dom/quota/AssertionsImpl.h"
#include "mozilla/dom/quota/CheckedUnsafePtr.h"
#include "mozilla/dom/quota/Client.h"
#include "mozilla/dom/quota/Constants.h"
#include "mozilla/dom/quota/DirectoryLock.h"
#include "mozilla/dom/quota/FileUtils.h"
#include "mozilla/dom/quota/PersistenceType.h"
#include "mozilla/dom/quota/QuotaManagerService.h"
#include "mozilla/dom/quota/ResultExtensions.h"
#include "mozilla/dom/quota/ScopedLogExtraInfo.h"
#include "mozilla/dom/quota/StreamUtils.h"
#include "mozilla/dom/simpledb/ActorsParent.h"
#include "mozilla/fallible.h"
#include "mozilla/ipc/BackgroundChild.h"
#include "mozilla/ipc/BackgroundParent.h"
#include "mozilla/ipc/PBackgroundChild.h"
#include "mozilla/ipc/PBackgroundSharedTypes.h"
#include "mozilla/ipc/ProtocolUtils.h"
#include "mozilla/net/ExtensionProtocolHandler.h"
#include "nsAppDirectoryServiceDefs.h"
#include "nsBaseHashtable.h"
#include "nsCOMPtr.h"
#include "nsCRTGlue.h"
#include "nsCharSeparatedTokenizer.h"
#include "nsClassHashtable.h"
#include "nsComponentManagerUtils.h"
#include "nsContentUtils.h"
#include "nsDebug.h"
#include "nsDirectoryServiceUtils.h"
#include "nsError.h"
#include "nsIBinaryInputStream.h"
#include "nsIBinaryOutputStream.h"
#include "nsIConsoleService.h"
#include "nsIDirectoryEnumerator.h"
#include "nsIDUtils.h"
#include "nsIEventTarget.h"
#include "nsIFile.h"
#include "nsIFileStreams.h"
#include "nsIInputStream.h"
#include "nsIObjectInputStream.h"
#include "nsIObjectOutputStream.h"
#include "nsIObserver.h"
#include "nsIObserverService.h"
#include "nsIOutputStream.h"
#include "nsIQuotaRequests.h"
#include "nsIPlatformInfo.h"
#include "nsIPrincipal.h"
#include "nsIRunnable.h"
#include "nsIScriptObjectPrincipal.h"
#include "nsISupports.h"
#include "nsISupportsPrimitives.h"
#include "nsIThread.h"
#include "nsITimer.h"
#include "nsIURI.h"
#include "nsIWidget.h"
#include "nsLiteralString.h"
#include "nsNetUtil.h"
#include "nsPIDOMWindow.h"
#include "nsPrintfCString.h"
#include "nsStandardURL.h"
#include "nsServiceManagerUtils.h"
#include "nsString.h"
#include "nsStringFlags.h"
#include "nsStringFwd.h"
#include "nsTArray.h"
#include "nsTHashtable.h"
#include "nsTLiteralString.h"
#include "nsTPromiseFlatString.h"
#include "nsTStringRepr.h"
#include "nsThreadUtils.h"
#include "nsURLHelper.h"
#include "nsXPCOM.h"
#include "nsXPCOMCID.h"
#include "nsXULAppAPI.h"
#include "prinrval.h"
#include "prio.h"
#include "prtime.h"

// The amount of time, in milliseconds, that our IO thread will stay alive
// after the last event it processes.
#define DEFAULT_THREAD_TIMEOUT_MS 30000

/**
 * If shutdown takes this long, kill actors of a quota client, to avoid reaching
 * the crash timeout.
 */
#define SHUTDOWN_KILL_ACTORS_TIMEOUT_MS 5000

/**
 * Automatically crash the browser if shutdown of a quota client takes this
 * long. We've chosen a value that is long enough that it is unlikely for the
 * problem to be falsely triggered by slow system I/O.  We've also chosen a
 * value long enough so that automated tests should time out and fail if
 * shutdown of a quota client takes too long.  Also, this value is long enough
 * so that testers can notice the timeout; we want to know about the timeouts,
 * not hide them. On the other hand this value is less than 60 seconds which is
 * used by nsTerminator to crash a hung main process.
 */
#define SHUTDOWN_CRASH_BROWSER_TIMEOUT_MS 45000

static_assert(
    SHUTDOWN_CRASH_BROWSER_TIMEOUT_MS > SHUTDOWN_KILL_ACTORS_TIMEOUT_MS,
    "The kill actors timeout must be shorter than the crash browser one.");

// profile-before-change, when we need to shut down quota manager
#define PROFILE_BEFORE_CHANGE_QM_OBSERVER_ID "profile-before-change-qm"

#define KB *1024ULL
#define MB *1024ULL KB
#define GB *1024ULL MB

namespace mozilla::dom::quota {

using namespace mozilla::ipc;

// We want profiles to be platform-independent so we always need to replace
// the same characters on every platform. Windows has the most extensive set
// of illegal characters so we use its FILE_ILLEGAL_CHARACTERS and
// FILE_PATH_SEPARATOR.
const char QuotaManager::kReplaceChars[] = CONTROL_CHARACTERS "/:*?\"<>|\\";
const char16_t QuotaManager::kReplaceChars16[] =
    u"" CONTROL_CHARACTERS "/:*?\"<>|\\";

namespace {

/*******************************************************************************
 * Constants
 ******************************************************************************/

const uint32_t kSQLitePageSizeOverride = 512;

// Important version history:
// - Bug 1290481 bumped our schema from major.minor 2.0 to 3.0 in Firefox 57
//   which caused Firefox 57 release concerns because the major schema upgrade
//   means anyone downgrading to Firefox 56 will experience a non-operational
//   QuotaManager and all of its clients.
// - Bug 1404344 got very concerned about that and so we decided to effectively
//   rename 3.0 to 2.1, effective in Firefox 57.  This works because post
//   storage.sqlite v1.0, QuotaManager doesn't care about minor storage version
//   increases.  It also works because all the upgrade did was give the DOM
//   Cache API QuotaClient an opportunity to create its newly added .padding
//   files during initialization/upgrade, which isn't functionally necessary as
//   that can be done on demand.

// Major storage version. Bump for backwards-incompatible changes.
// (The next major version should be 4 to distinguish from the Bug 1290481
// downgrade snafu.)
const uint32_t kMajorStorageVersion = 2;

// Minor storage version. Bump for backwards-compatible changes.
const uint32_t kMinorStorageVersion = 3;

// The storage version we store in the SQLite database is a (signed) 32-bit
// integer. The major version is left-shifted 16 bits so the max value is
// 0xFFFF. The minor version occupies the lower 16 bits and its max is 0xFFFF.
static_assert(kMajorStorageVersion <= 0xFFFF,
              "Major version needs to fit in 16 bits.");
static_assert(kMinorStorageVersion <= 0xFFFF,
              "Minor version needs to fit in 16 bits.");

const int32_t kStorageVersion =
    int32_t((kMajorStorageVersion << 16) + kMinorStorageVersion);

// See comments above about why these are a thing.
const int32_t kHackyPreDowngradeStorageVersion = int32_t((3 << 16) + 0);
const int32_t kHackyPostDowngradeStorageVersion = int32_t((2 << 16) + 1);

const char kAboutHomeOriginPrefix[] = "moz-safe-about:home";
const char kIndexedDBOriginPrefix[] = "indexeddb://";
const char kResourceOriginPrefix[] = "resource://";

constexpr auto kStorageName = u"storage"_ns;

#define INDEXEDDB_DIRECTORY_NAME u"indexedDB"
#define ARCHIVES_DIRECTORY_NAME u"archives"
#define PERSISTENT_DIRECTORY_NAME u"persistent"
#define PERMANENT_DIRECTORY_NAME u"permanent"
#define TEMPORARY_DIRECTORY_NAME u"temporary"
#define DEFAULT_DIRECTORY_NAME u"default"
#define DEFAULT_PRIVATE_DIRECTORY_NAME u"private"

#define WEB_APPS_STORE_FILE_NAME u"webappsstore.sqlite"
#define LS_ARCHIVE_FILE_NAME u"ls-archive.sqlite"
#define LS_ARCHIVE_TMP_FILE_NAME u"ls-archive-tmp.sqlite"

const int32_t kLocalStorageArchiveVersion = 4;

const char kProfileDoChangeTopic[] = "profile-do-change";
const char kPrivateBrowsingObserverTopic[] = "last-pb-context-exited";

const int32_t kCacheVersion = 2;

/******************************************************************************
 * SQLite functions
 ******************************************************************************/

int32_t MakeStorageVersion(uint32_t aMajorStorageVersion,
                           uint32_t aMinorStorageVersion) {
  return int32_t((aMajorStorageVersion << 16) + aMinorStorageVersion);
}

uint32_t GetMajorStorageVersion(int32_t aStorageVersion) {
  return uint32_t(aStorageVersion >> 16);
}

nsresult CreateTables(mozIStorageConnection* aConnection) {
  AssertIsOnIOThread();
  MOZ_ASSERT(aConnection);

  // Table `database`
  QM_TRY(MOZ_TO_RESULT(
      aConnection->ExecuteSimpleSQL("CREATE TABLE database"
                                    "( cache_version INTEGER NOT NULL DEFAULT 0"
                                    ");"_ns)));

#ifdef DEBUG
  {
    QM_TRY_INSPECT(const int32_t& storageVersion,
                   MOZ_TO_RESULT_INVOKE_MEMBER(aConnection, GetSchemaVersion));

    MOZ_ASSERT(storageVersion == 0);
  }
#endif

  QM_TRY(MOZ_TO_RESULT(aConnection->SetSchemaVersion(kStorageVersion)));

  return NS_OK;
}

Result<int32_t, nsresult> LoadCacheVersion(mozIStorageConnection& aConnection) {
  AssertIsOnIOThread();

  QM_TRY_INSPECT(const auto& stmt,
                 CreateAndExecuteSingleStepStatement<
                     SingleStepResult::ReturnNullIfNoResult>(
                     aConnection, "SELECT cache_version FROM database"_ns));

  QM_TRY(OkIf(stmt), Err(NS_ERROR_FILE_CORRUPTED));

  QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt32, 0));
}

nsresult SaveCacheVersion(mozIStorageConnection& aConnection,
                          int32_t aVersion) {
  AssertIsOnIOThread();

  QM_TRY_INSPECT(
      const auto& stmt,
      MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
          nsCOMPtr<mozIStorageStatement>, aConnection, CreateStatement,
          "UPDATE database SET cache_version = :version;"_ns));

  QM_TRY(MOZ_TO_RESULT(stmt->BindInt32ByName("version"_ns, aVersion)));

  QM_TRY(MOZ_TO_RESULT(stmt->Execute()));

  return NS_OK;
}

nsresult CreateCacheTables(mozIStorageConnection& aConnection) {
  AssertIsOnIOThread();

  // Table `cache`
  QM_TRY(MOZ_TO_RESULT(
      aConnection.ExecuteSimpleSQL("CREATE TABLE cache"
                                   "( valid INTEGER NOT NULL DEFAULT 0"
                                   ", build_id TEXT NOT NULL DEFAULT ''"
                                   ");"_ns)));

  // Table `repository`
  QM_TRY(
      MOZ_TO_RESULT(aConnection.ExecuteSimpleSQL("CREATE TABLE repository"
                                                 "( id INTEGER PRIMARY KEY"
                                                 ", name TEXT NOT NULL"
                                                 ");"_ns)));

  // Table `origin`
  QM_TRY(MOZ_TO_RESULT(
      aConnection.ExecuteSimpleSQL("CREATE TABLE origin"
                                   "( repository_id INTEGER NOT NULL"
                                   ", suffix TEXT"
                                   ", group_ TEXT NOT NULL"
                                   ", origin TEXT NOT NULL"
                                   ", client_usages TEXT NOT NULL"
                                   ", usage INTEGER NOT NULL"
                                   ", last_access_time INTEGER NOT NULL"
                                   ", accessed INTEGER NOT NULL"
                                   ", persisted INTEGER NOT NULL"
                                   ", PRIMARY KEY (repository_id, origin)"
                                   ", FOREIGN KEY (repository_id) "
                                   "REFERENCES repository(id) "
                                   ");"_ns)));

#ifdef DEBUG
  {
    QM_TRY_INSPECT(const int32_t& cacheVersion, LoadCacheVersion(aConnection));
    MOZ_ASSERT(cacheVersion == 0);
  }
#endif

  QM_TRY(MOZ_TO_RESULT(SaveCacheVersion(aConnection, kCacheVersion)));

  return NS_OK;
}

OkOrErr InvalidateCache(mozIStorageConnection& aConnection) {
  AssertIsOnIOThread();

  static constexpr auto kDeleteCacheQuery = "DELETE FROM origin;"_ns;
  static constexpr auto kSetInvalidFlagQuery = "UPDATE cache SET valid = 0"_ns;

  QM_TRY(QM_OR_ELSE_WARN(
      // Expression.
      ([&]() -> OkOrErr {
        mozStorageTransaction transaction(&aConnection,
                                          /*aCommitOnComplete */ false);

        QM_TRY(QM_TO_RESULT(transaction.Start()));
        QM_TRY(QM_TO_RESULT(aConnection.ExecuteSimpleSQL(kDeleteCacheQuery)));
        QM_TRY(
            QM_TO_RESULT(aConnection.ExecuteSimpleSQL(kSetInvalidFlagQuery)));
        QM_TRY(QM_TO_RESULT(transaction.Commit()));

        return Ok{};
      }()),
      // Fallback.
      ([&](const QMResult& rv) -> OkOrErr {
        QM_TRY(
            QM_TO_RESULT(aConnection.ExecuteSimpleSQL(kSetInvalidFlagQuery)));

        return Ok{};
      })));

  return Ok{};
}

nsresult UpgradeCacheFrom1To2(mozIStorageConnection& aConnection) {
  AssertIsOnIOThread();

  QM_TRY(MOZ_TO_RESULT(aConnection.ExecuteSimpleSQL(
      "ALTER TABLE origin ADD COLUMN suffix TEXT"_ns)));

  QM_TRY(InvalidateCache(aConnection));

#ifdef DEBUG
  {
    QM_TRY_INSPECT(const int32_t& cacheVersion, LoadCacheVersion(aConnection));

    MOZ_ASSERT(cacheVersion == 1);
  }
#endif

  QM_TRY(MOZ_TO_RESULT(SaveCacheVersion(aConnection, 2)));

  return NS_OK;
}

Result<bool, nsresult> MaybeCreateOrUpgradeCache(
    mozIStorageConnection& aConnection) {
  bool cacheUsable = true;

  QM_TRY_UNWRAP(int32_t cacheVersion, LoadCacheVersion(aConnection));

  if (cacheVersion > kCacheVersion) {
    cacheUsable = false;
  } else if (cacheVersion != kCacheVersion) {
    const bool newCache = !cacheVersion;

    mozStorageTransaction transaction(
        &aConnection, false, mozIStorageConnection::TRANSACTION_IMMEDIATE);

    QM_TRY(MOZ_TO_RESULT(transaction.Start()));

    if (newCache) {
      QM_TRY(MOZ_TO_RESULT(CreateCacheTables(aConnection)));

#ifdef DEBUG
      {
        QM_TRY_INSPECT(const int32_t& cacheVersion,
                       LoadCacheVersion(aConnection));
        MOZ_ASSERT(cacheVersion == kCacheVersion);
      }
#endif

      QM_TRY(MOZ_TO_RESULT(aConnection.ExecuteSimpleSQL(
          nsLiteralCString("INSERT INTO cache (valid, build_id) "
                           "VALUES (0, '')"))));

      nsCOMPtr<mozIStorageStatement> insertStmt;

      for (const PersistenceType persistenceType : kAllPersistenceTypes) {
        if (insertStmt) {
          MOZ_ALWAYS_SUCCEEDS(insertStmt->Reset());
        } else {
          QM_TRY_UNWRAP(insertStmt, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
                                        nsCOMPtr<mozIStorageStatement>,
                                        aConnection, CreateStatement,
                                        "INSERT INTO repository (id, name) "
                                        "VALUES (:id, :name)"_ns));
        }

        QM_TRY(MOZ_TO_RESULT(
            insertStmt->BindInt32ByName("id"_ns, persistenceType)));

        QM_TRY(MOZ_TO_RESULT(insertStmt->BindUTF8StringByName(
            "name"_ns, PersistenceTypeToString(persistenceType))));

        QM_TRY(MOZ_TO_RESULT(insertStmt->Execute()));
      }
    } else {
      // This logic needs to change next time we change the cache!
      static_assert(kCacheVersion == 2,
                    "Upgrade function needed due to cache version increase.");

      while (cacheVersion != kCacheVersion) {
        if (cacheVersion == 1) {
          QM_TRY(MOZ_TO_RESULT(UpgradeCacheFrom1To2(aConnection)));
        } else {
          QM_FAIL(Err(NS_ERROR_FAILURE), []() {
            QM_WARNING(
                "Unable to initialize cache, no upgrade path is "
                "available!");
          });
        }

        QM_TRY_UNWRAP(cacheVersion, LoadCacheVersion(aConnection));
      }

      MOZ_ASSERT(cacheVersion == kCacheVersion);
    }

    QM_TRY(MOZ_TO_RESULT(transaction.Commit()));
  }

  return cacheUsable;
}

Result<nsCOMPtr<mozIStorageConnection>, nsresult> CreateWebAppsStoreConnection(
    nsIFile& aWebAppsStoreFile, mozIStorageService& aStorageService) {
  AssertIsOnIOThread();

  // Check if the old database exists at all.
  QM_TRY_INSPECT(const bool& exists,
                 MOZ_TO_RESULT_INVOKE_MEMBER(aWebAppsStoreFile, Exists));

  if (!exists) {
    // webappsstore.sqlite doesn't exist, return a null connection.
    return nsCOMPtr<mozIStorageConnection>{};
  }

  QM_TRY_INSPECT(const bool& isDirectory,
                 MOZ_TO_RESULT_INVOKE_MEMBER(aWebAppsStoreFile, IsDirectory));

  if (isDirectory) {
    QM_WARNING("webappsstore.sqlite is not a file!");
    return nsCOMPtr<mozIStorageConnection>{};
  }

  QM_TRY_INSPECT(const auto& connection,
                 QM_OR_ELSE_WARN_IF(
                     // Expression.
                     MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
                         nsCOMPtr<mozIStorageConnection>, aStorageService,
                         OpenUnsharedDatabase, &aWebAppsStoreFile,
                         mozIStorageService::CONNECTION_DEFAULT),
                     // Predicate.
                     IsDatabaseCorruptionError,
                     // Fallback. Don't throw an error, leave a corrupted
                     // webappsstore database as it is.
                     ErrToDefaultOk<nsCOMPtr<mozIStorageConnection>>));

  if (connection) {
    // Don't propagate an error, leave a non-updateable webappsstore database as
    // it is.
    QM_TRY(MOZ_TO_RESULT(StorageDBUpdater::Update(connection)),
           nsCOMPtr<mozIStorageConnection>{});
  }

  return connection;
}

Result<nsCOMPtr<nsIFile>, QMResult> GetLocalStorageArchiveFile(
    const nsAString& aDirectoryPath) {
  AssertIsOnIOThread();
  MOZ_ASSERT(!aDirectoryPath.IsEmpty());

  QM_TRY_UNWRAP(auto lsArchiveFile,
                QM_TO_RESULT_TRANSFORM(QM_NewLocalFile(aDirectoryPath)));

  QM_TRY(QM_TO_RESULT(
      lsArchiveFile->Append(nsLiteralString(LS_ARCHIVE_FILE_NAME))));

  return lsArchiveFile;
}

Result<nsCOMPtr<nsIFile>, nsresult> GetLocalStorageArchiveTmpFile(
    const nsAString& aDirectoryPath) {
  AssertIsOnIOThread();
  MOZ_ASSERT(!aDirectoryPath.IsEmpty());

  QM_TRY_UNWRAP(auto lsArchiveTmpFile, QM_NewLocalFile(aDirectoryPath));

  QM_TRY(MOZ_TO_RESULT(
      lsArchiveTmpFile->Append(nsLiteralString(LS_ARCHIVE_TMP_FILE_NAME))));

  return lsArchiveTmpFile;
}

Result<bool, nsresult> IsLocalStorageArchiveInitialized(
    mozIStorageConnection& aConnection) {
  AssertIsOnIOThread();

  QM_TRY_RETURN(
      MOZ_TO_RESULT_INVOKE_MEMBER(aConnection, TableExists, "database"_ns));
}

nsresult InitializeLocalStorageArchive(mozIStorageConnection* aConnection) {
  AssertIsOnIOThread();
  MOZ_ASSERT(aConnection);

#ifdef DEBUG
  {
    QM_TRY_INSPECT(const auto& initialized,
                   IsLocalStorageArchiveInitialized(*aConnection));
    MOZ_ASSERT(!initialized);
  }
#endif

  QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL(
      "CREATE TABLE database(version INTEGER NOT NULL DEFAULT 0);"_ns)));

  QM_TRY_INSPECT(
      const auto& stmt,
      MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
          nsCOMPtr<mozIStorageStatement>, aConnection, CreateStatement,
          "INSERT INTO database (version) VALUES (:version)"_ns));

  QM_TRY(MOZ_TO_RESULT(stmt->BindInt32ByName("version"_ns, 0)));
  QM_TRY(MOZ_TO_RESULT(stmt->Execute()));

  return NS_OK;
}

Result<int32_t, nsresult> LoadLocalStorageArchiveVersion(
    mozIStorageConnection& aConnection) {
  AssertIsOnIOThread();

  QM_TRY_INSPECT(const auto& stmt,
                 CreateAndExecuteSingleStepStatement<
                     SingleStepResult::ReturnNullIfNoResult>(
                     aConnection, "SELECT version FROM database"_ns));

  QM_TRY(OkIf(stmt), Err(NS_ERROR_FILE_CORRUPTED));

  QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt32, 0));
}

nsresult SaveLocalStorageArchiveVersion(mozIStorageConnection* aConnection,
                                        int32_t aVersion) {
  AssertIsOnIOThread();
  MOZ_ASSERT(aConnection);

  nsCOMPtr<mozIStorageStatement> stmt;
  nsresult rv = aConnection->CreateStatement(
      "UPDATE database SET version = :version;"_ns, getter_AddRefs(stmt));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = stmt->BindInt32ByName("version"_ns, aVersion);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = stmt->Execute();
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

template <typename FileFunc, typename DirectoryFunc>
Result<mozilla::Ok, nsresult> CollectEachFileEntry(
    nsIFile& aDirectory, const FileFunc& aFileFunc,
    const DirectoryFunc& aDirectoryFunc) {
  AssertIsOnIOThread();

  return CollectEachFile(
      aDirectory,
      [&aFileFunc, &aDirectoryFunc](
          const nsCOMPtr<nsIFile>& file) -> Result<mozilla::Ok, nsresult> {
        QM_TRY_INSPECT(const auto& dirEntryKind, GetDirEntryKind(*file));

        switch (dirEntryKind) {
          case nsIFileKind::ExistsAsDirectory:
            return aDirectoryFunc(file);

          case nsIFileKind::ExistsAsFile:
            return aFileFunc(file);

          case nsIFileKind::DoesNotExist:
            // Ignore files that got removed externally while iterating.
            break;
        }

        return Ok{};
      });
}

/******************************************************************************
 * Quota manager class declarations
 ******************************************************************************/

}  // namespace

class QuotaManager::Observer final : public nsIObserver {
  static Observer* sInstance;

  bool mPendingProfileChange;
  bool mShutdownComplete;

 public:
  static nsresult Initialize();

  static nsIObserver* GetInstance();

  static void ShutdownCompleted();

 private:
  Observer() : mPendingProfileChange(false), mShutdownComplete(false) {
    MOZ_ASSERT(NS_IsMainThread());
  }

  ~Observer() { MOZ_ASSERT(NS_IsMainThread()); }

  nsresult Init();

  nsresult Shutdown();

  NS_DECL_ISUPPORTS
  NS_DECL_NSIOBSERVER
};

namespace {

/*******************************************************************************
 * Local class declarations
 ******************************************************************************/

}  // namespace

namespace {

class CollectOriginsHelper final : public Runnable {
  uint64_t mMinSizeToBeFreed;

  Mutex& mMutex;
  CondVar mCondVar;

  // The members below are protected by mMutex.
  nsTArray<RefPtr<OriginDirectoryLock>> mLocks;
  uint64_t mSizeToBeFreed;
  bool mWaiting;

 public:
  CollectOriginsHelper(mozilla::Mutex& aMutex, uint64_t aMinSizeToBeFreed);

  // Blocks the current thread until origins are collected on the main thread.
  // The returned value contains an aggregate size of those origins.
  int64_t BlockAndReturnOriginsForEviction(
      nsTArray<RefPtr<OriginDirectoryLock>>& aLocks);

 private:
  ~CollectOriginsHelper() = default;

  NS_IMETHOD
  Run() override;
};

/*******************************************************************************
 * Other class declarations
 ******************************************************************************/

class StoragePressureRunnable final : public Runnable {
  const uint64_t mUsage;

 public:
  explicit StoragePressureRunnable(uint64_t aUsage)
      : Runnable("dom::quota::QuotaObject::StoragePressureRunnable"),
        mUsage(aUsage) {}

 private:
  ~StoragePressureRunnable() = default;

  NS_DECL_NSIRUNNABLE
};

class RecordQuotaInfoLoadTimeHelper final : public Runnable {
  // TimeStamps that are set on the IO thread.
  LazyInitializedOnceNotNull<const TimeStamp> mStartTime;
  LazyInitializedOnceNotNull<const TimeStamp> mEndTime;

  // A TimeStamp that is set on the main thread.
  LazyInitializedOnceNotNull<const TimeStamp> mInitializedTime;

 public:
  RecordQuotaInfoLoadTimeHelper()
      : Runnable("dom::quota::RecordQuotaInfoLoadTimeHelper") {}

  TimeStamp Start();

  TimeStamp End();

 private:
  ~RecordQuotaInfoLoadTimeHelper() = default;

  NS_DECL_NSIRUNNABLE
};

/*******************************************************************************
 * Helper classes
 ******************************************************************************/

/*******************************************************************************
 * Helper Functions
 ******************************************************************************/

// Return whether the group was actually updated.
Result<bool, nsresult> MaybeUpdateGroupForOrigin(
    OriginMetadata& aOriginMetadata) {
  MOZ_ASSERT(!NS_IsMainThread());

  bool updated = false;

  if (aOriginMetadata.mOrigin.EqualsLiteral(kChromeOrigin)) {
    if (!aOriginMetadata.mGroup.EqualsLiteral(kChromeOrigin)) {
      aOriginMetadata.mGroup.AssignLiteral(kChromeOrigin);
      updated = true;
    }
  } else {
    nsCOMPtr<nsIPrincipal> principal =
        BasePrincipal::CreateContentPrincipal(aOriginMetadata.mOrigin);
    QM_TRY(MOZ_TO_RESULT(principal));

    QM_TRY_INSPECT(const auto& baseDomain,
                   MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoCString, principal,
                                                     GetBaseDomain));

    const nsCString upToDateGroup = baseDomain + aOriginMetadata.mSuffix;

    if (aOriginMetadata.mGroup != upToDateGroup) {
      aOriginMetadata.mGroup = upToDateGroup;
      updated = true;
    }
  }

  return updated;
}

Result<bool, nsresult> MaybeUpdateLastAccessTimeForOrigin(
    FullOriginMetadata& aFullOriginMetadata) {
  MOZ_ASSERT(!NS_IsMainThread());

  if (aFullOriginMetadata.mLastAccessTime == INT64_MIN) {
    QuotaManager* quotaManager = QuotaManager::Get();
    MOZ_ASSERT(quotaManager);

    QM_TRY_INSPECT(const auto& metadataFile,
                   quotaManager->GetOriginDirectory(aFullOriginMetadata));

    QM_TRY(MOZ_TO_RESULT(
        metadataFile->Append(nsLiteralString(METADATA_V2_FILE_NAME))));

    QM_TRY_UNWRAP(int64_t timestamp, MOZ_TO_RESULT_INVOKE_MEMBER(
                                         metadataFile, GetLastModifiedTime));

    // Need to convert from milliseconds to microseconds.
    MOZ_ASSERT((INT64_MAX / PR_USEC_PER_MSEC) > timestamp);
    timestamp *= int64_t(PR_USEC_PER_MSEC);

    aFullOriginMetadata.mLastAccessTime = timestamp;

    return true;
  }

  return false;
}

}  // namespace

BackgroundThreadObject::BackgroundThreadObject()
    : mOwningThread(GetCurrentSerialEventTarget()) {
  AssertIsOnOwningThread();
}

BackgroundThreadObject::BackgroundThreadObject(
    nsISerialEventTarget* aOwningThread)
    : mOwningThread(aOwningThread) {}

#ifdef DEBUG

void BackgroundThreadObject::AssertIsOnOwningThread() const {
  AssertIsOnBackgroundThread();
  MOZ_ASSERT(mOwningThread);
  bool current;
  MOZ_ASSERT(NS_SUCCEEDED(mOwningThread->IsOnCurrentThread(&current)));
  MOZ_ASSERT(current);
}

#endif  // DEBUG

nsISerialEventTarget* BackgroundThreadObject::OwningThread() const {
  MOZ_ASSERT(mOwningThread);
  return mOwningThread;
}

void ReportInternalError(const char* aFile, uint32_t aLine, const char* aStr) {
  // Get leaf of file path
  for (const char* p = aFile; *p; ++p) {
    if (*p == '/' && *(p + 1)) {
      aFile = p + 1;
    }
  }

  nsContentUtils::LogSimpleConsoleError(
      NS_ConvertUTF8toUTF16(
          nsPrintfCString("Quota %s: %s:%" PRIu32, aStr, aFile, aLine)),
      "quota"_ns,
      false /* Quota Manager is not active in private browsing mode */,
      true /* Quota Manager runs always in a chrome context */);
}

namespace {

bool gInvalidateQuotaCache = false;
StaticAutoPtr<nsString> gBasePath;
StaticAutoPtr<nsString> gStorageName;
StaticAutoPtr<nsCString> gBuildId;

#ifdef DEBUG
bool gQuotaManagerInitialized = false;
#endif

StaticRefPtr<QuotaManager> gInstance;
mozilla::Atomic<bool> gShutdown(false);

// A time stamp that can only be accessed on the main thread.
TimeStamp gLastOSWake;

// XXX Move to QuotaManager once NormalOriginOperationBase is declared in a
// separate and includable file.
using NormalOriginOpArray =
    nsTArray<CheckedUnsafePtr<NormalOriginOperationBase>>;
StaticAutoPtr<NormalOriginOpArray> gNormalOriginOps;

class StorageOperationBase {
 protected:
  struct OriginProps {
    enum Type { eChrome, eContent, eObsolete, eInvalid };

    NotNull<nsCOMPtr<nsIFile>> mDirectory;
    nsString mLeafName;
    nsCString mSpec;
    OriginAttributes mAttrs;
    int64_t mTimestamp;
    OriginMetadata mOriginMetadata;
    nsCString mOriginalSuffix;

    LazyInitializedOnceEarlyDestructible<const PersistenceType>
        mPersistenceType;
    Type mType;
    bool mNeedsRestore;
    bool mNeedsRestore2;
    bool mIgnore;

   public:
    explicit OriginProps(MovingNotNull<nsCOMPtr<nsIFile>> aDirectory)
        : mDirectory(std::move(aDirectory)),
          mTimestamp(0),
          mType(eContent),
          mNeedsRestore(false),
          mNeedsRestore2(false),
          mIgnore(false) {}

    template <typename PersistenceTypeFunc>
    nsresult Init(PersistenceTypeFunc&& aPersistenceTypeFunc);
  };

  nsTArray<OriginProps> mOriginProps;

  nsCOMPtr<nsIFile> mDirectory;

 public:
  explicit StorageOperationBase(nsIFile* aDirectory) : mDirectory(aDirectory) {
    AssertIsOnIOThread();
  }

  NS_INLINE_DECL_REFCOUNTING(StorageOperationBase)

 protected:
  virtual ~StorageOperationBase() = default;

  nsresult GetDirectoryMetadata(nsIFile* aDirectory, int64_t& aTimestamp,
                                nsACString& aGroup, nsACString& aOrigin,
                                Nullable<bool>& aIsApp);

  // Upgrade helper to load the contents of ".metadata-v2" files from previous
  // schema versions.  Although QuotaManager has a similar GetDirectoryMetadata2
  // method, it is only intended to read current version ".metadata-v2" files.
  // And unlike the old ".metadata" files, the ".metadata-v2" format can evolve
  // because our "storage.sqlite" lets us track the overall version of the
  // storage directory.
  nsresult GetDirectoryMetadata2(nsIFile* aDirectory, int64_t& aTimestamp,
                                 nsACString& aSuffix, nsACString& aGroup,
                                 nsACString& aOrigin, bool& aIsApp);

  int64_t GetOriginLastModifiedTime(const OriginProps& aOriginProps);

  nsresult RemoveObsoleteOrigin(const OriginProps& aOriginProps);

  /**
   * Rename the origin if the origin string generation from nsIPrincipal
   * changed. This consists of renaming the origin in the metadata files and
   * renaming the origin directory itself. For simplicity, the origin in
   * metadata files is not actually updated, but the metadata files are
   * recreated instead.
   *
   * @param  aOriginProps the properties of the origin to check.
   *
   * @return whether origin was renamed.
   */
  Result<bool, nsresult> MaybeRenameOrigin(const OriginProps& aOriginProps);

  nsresult ProcessOriginDirectories();

  virtual nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) = 0;
};

class MOZ_STACK_CLASS OriginParser final {
 public:
  enum ResultType { InvalidOrigin, ObsoleteOrigin, ValidOrigin };

 private:
  using Tokenizer =
      nsCCharSeparatedTokenizerTemplate<NS_TokenizerIgnoreNothing>;

  enum SchemeType { eNone, eFile, eAbout, eChrome };

  enum State {
    eExpectingAppIdOrScheme,
    eExpectingInMozBrowser,
    eExpectingScheme,
    eExpectingEmptyToken1,
    eExpectingEmptyToken2,
    eExpectingEmptyTokenOrUniversalFileOrigin,
    eExpectingHost,
    eExpectingPort,
    eExpectingEmptyTokenOrDriveLetterOrPathnameComponent,
    eExpectingEmptyTokenOrPathnameComponent,
    eExpectingEmptyToken1OrHost,

    // We transit from eExpectingHost to this state when we encounter a host
    // beginning with "[" which indicates an IPv6 literal. Because we mangle the
    // IPv6 ":" delimiter to be a "+", we will receive separate tokens for each
    // portion of the IPv6 address, including a final token that ends with "]".
    // (Note that we do not mangle "[" or "]".) Note that the URL spec
    // explicitly disclaims support for "<zone_id>" and so we don't have to deal
    // with that.
    eExpectingIPV6Token,
    eComplete,
    eHandledTrailingSeparator
  };

  const nsCString mOrigin;
  Tokenizer mTokenizer;

  nsCString mScheme;
  nsCString mHost;
  Nullable<uint32_t> mPort;
  nsTArray<nsCString> mPathnameComponents;
  nsCString mHandledTokens;

  SchemeType mSchemeType;
  State mState;
  bool mInIsolatedMozBrowser;
  bool mUniversalFileOrigin;
  bool mMaybeDriveLetter;
  bool mError;
  bool mMaybeObsolete;

  // Number of group which a IPv6 address has. Should be less than 9.
  uint8_t mIPGroup;

 public:
  explicit OriginParser(const nsACString& aOrigin)
      : mOrigin(aOrigin),
        mTokenizer(aOrigin, '+'),
        mPort(),
        mSchemeType(eNone),
        mState(eExpectingAppIdOrScheme),
        mInIsolatedMozBrowser(false),
        mUniversalFileOrigin(false),
        mMaybeDriveLetter(false),
        mError(false),
        mMaybeObsolete(false),
        mIPGroup(0) {}

  static ResultType ParseOrigin(const nsACString& aOrigin, nsCString& aSpec,
                                OriginAttributes* aAttrs,
                                nsCString& aOriginalSuffix);

  ResultType Parse(nsACString& aSpec);

 private:
  void HandleScheme(const nsDependentCSubstring& aToken);

  void HandlePathnameComponent(const nsDependentCSubstring& aToken);

  void HandleToken(const nsDependentCSubstring& aToken);

  void HandleTrailingSeparator();
};

class RepositoryOperationBase : public StorageOperationBase {
 public:
  explicit RepositoryOperationBase(nsIFile* aDirectory)
      : StorageOperationBase(aDirectory) {}

  nsresult ProcessRepository();

 protected:
  virtual ~RepositoryOperationBase() = default;

  template <typename UpgradeMethod>
  nsresult MaybeUpgradeClients(const OriginProps& aOriginsProps,
                               UpgradeMethod aMethod);

 private:
  virtual PersistenceType PersistenceTypeFromSpec(const nsCString& aSpec) = 0;

  virtual nsresult PrepareOriginDirectory(OriginProps& aOriginProps,
                                          bool* aRemoved) = 0;

  virtual nsresult PrepareClientDirectory(nsIFile* aFile,
                                          const nsAString& aLeafName,
                                          bool& aRemoved);
};

class CreateOrUpgradeDirectoryMetadataHelper final
    : public RepositoryOperationBase {
  nsCOMPtr<nsIFile> mPermanentStorageDir;

  // The legacy PersistenceType, before the default repository introduction.
  enum class LegacyPersistenceType {
    Persistent = 0,
    Temporary
    // The PersistenceType had also PERSISTENCE_TYPE_INVALID, but we don't need
    // it here.
  };

  LazyInitializedOnce<const LegacyPersistenceType> mLegacyPersistenceType;

 public:
  explicit CreateOrUpgradeDirectoryMetadataHelper(nsIFile* aDirectory)
      : RepositoryOperationBase(aDirectory) {}

  nsresult Init();

 private:
  Maybe<LegacyPersistenceType> LegacyPersistenceTypeFromFile(nsIFile& aFile,
                                                             const fallible_t&);

  PersistenceType PersistenceTypeFromLegacyPersistentSpec(
      const nsCString& aSpec);

  PersistenceType PersistenceTypeFromSpec(const nsCString& aSpec) override;

  nsresult MaybeUpgradeOriginDirectory(nsIFile* aDirectory);

  nsresult PrepareOriginDirectory(OriginProps& aOriginProps,
                                  bool* aRemoved) override;

  nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) override;
};

class UpgradeStorageHelperBase : public RepositoryOperationBase {
  LazyInitializedOnce<const PersistenceType> mPersistenceType;

 public:
  explicit UpgradeStorageHelperBase(nsIFile* aDirectory)
      : RepositoryOperationBase(aDirectory) {}

  nsresult Init();

 private:
  PersistenceType PersistenceTypeFromSpec(const nsCString& aSpec) override;
};

class UpgradeStorageFrom0_0To1_0Helper final : public UpgradeStorageHelperBase {
 public:
  explicit UpgradeStorageFrom0_0To1_0Helper(nsIFile* aDirectory)
      : UpgradeStorageHelperBase(aDirectory) {}

 private:
  nsresult PrepareOriginDirectory(OriginProps& aOriginProps,
                                  bool* aRemoved) override;

  nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) override;
};

class UpgradeStorageFrom1_0To2_0Helper final : public UpgradeStorageHelperBase {
 public:
  explicit UpgradeStorageFrom1_0To2_0Helper(nsIFile* aDirectory)
      : UpgradeStorageHelperBase(aDirectory) {}

 private:
  nsresult MaybeRemoveMorgueDirectory(const OriginProps& aOriginProps);

  /**
   * Remove the origin directory if appId is present in origin attributes.
   *
   * @param aOriginProps the properties of the origin to check.
   *
   * @return whether the origin directory was removed.
   */
  Result<bool, nsresult> MaybeRemoveAppsData(const OriginProps& aOriginProps);

  nsresult PrepareOriginDirectory(OriginProps& aOriginProps,
                                  bool* aRemoved) override;

  nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) override;
};

class UpgradeStorageFrom2_0To2_1Helper final : public UpgradeStorageHelperBase {
 public:
  explicit UpgradeStorageFrom2_0To2_1Helper(nsIFile* aDirectory)
      : UpgradeStorageHelperBase(aDirectory) {}

 private:
  nsresult PrepareOriginDirectory(OriginProps& aOriginProps,
                                  bool* aRemoved) override;

  nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) override;
};

class UpgradeStorageFrom2_1To2_2Helper final : public UpgradeStorageHelperBase {
 public:
  explicit UpgradeStorageFrom2_1To2_2Helper(nsIFile* aDirectory)
      : UpgradeStorageHelperBase(aDirectory) {}

 private:
  nsresult PrepareOriginDirectory(OriginProps& aOriginProps,
                                  bool* aRemoved) override;

  nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) override;

  nsresult PrepareClientDirectory(nsIFile* aFile, const nsAString& aLeafName,
                                  bool& aRemoved) override;
};

class RestoreDirectoryMetadata2Helper final : public StorageOperationBase {
  LazyInitializedOnce<const PersistenceType> mPersistenceType;

 public:
  explicit RestoreDirectoryMetadata2Helper(nsIFile* aDirectory)
      : StorageOperationBase(aDirectory) {}

  nsresult Init();

  nsresult RestoreMetadata2File();

 private:
  nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) override;
};

Result<nsAutoString, nsresult> GetPathForStorage(
    nsIFile& aBaseDir, const nsAString& aStorageName) {
  QM_TRY_INSPECT(const auto& storageDir,
                 CloneFileAndAppend(aBaseDir, aStorageName));

  QM_TRY_RETURN(
      MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoString, storageDir, GetPath));
}

int64_t GetLastModifiedTime(PersistenceType aPersistenceType, nsIFile& aFile) {
  AssertIsOnIOThread();

  class MOZ_STACK_CLASS Helper final {
   public:
    static nsresult GetLastModifiedTime(nsIFile* aFile, int64_t* aTimestamp) {
      AssertIsOnIOThread();
      MOZ_ASSERT(aFile);
      MOZ_ASSERT(aTimestamp);

      QM_TRY_INSPECT(const auto& dirEntryKind, GetDirEntryKind(*aFile));

      switch (dirEntryKind) {
        case nsIFileKind::ExistsAsDirectory:
          QM_TRY(CollectEachFile(
              *aFile,
              [&aTimestamp](const nsCOMPtr<nsIFile>& file)
                  -> Result<mozilla::Ok, nsresult> {
                QM_TRY(MOZ_TO_RESULT(GetLastModifiedTime(file, aTimestamp)));

                return Ok{};
              }));
          break;

        case nsIFileKind::ExistsAsFile: {
          QM_TRY_INSPECT(const auto& leafName,
                         MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoString, aFile,
                                                           GetLeafName));

          // Bug 1595445 will handle unknown files here.

          if (IsOriginMetadata(leafName) || IsTempMetadata(leafName) ||
              IsDotFile(leafName)) {
            return NS_OK;
          }

          QM_TRY_UNWRAP(int64_t timestamp, MOZ_TO_RESULT_INVOKE_MEMBER(
                                               aFile, GetLastModifiedTime));

          // Need to convert from milliseconds to microseconds.
          MOZ_ASSERT((INT64_MAX / PR_USEC_PER_MSEC) > timestamp);
          timestamp *= int64_t(PR_USEC_PER_MSEC);

          if (timestamp > *aTimestamp) {
            *aTimestamp = timestamp;
          }
          break;
        }

        case nsIFileKind::DoesNotExist:
          // Ignore files that got removed externally while iterating.
          break;
      }

      return NS_OK;
    }
  };

  if (aPersistenceType == PERSISTENCE_TYPE_PERSISTENT) {
    return PR_Now();
  }

  int64_t timestamp = INT64_MIN;
  nsresult rv = Helper::GetLastModifiedTime(&aFile, &timestamp);
  if (NS_FAILED(rv)) {
    timestamp = PR_Now();
  }

  // XXX if there were no suitable files for getting last modified time
  // (timestamp is still set to INT64_MIN), we should return the current time
  // instead of returning INT64_MIN.

  return timestamp;
}

// Returns a bool indicating whether the directory was newly created.
Result<bool, nsresult> EnsureDirectory(nsIFile& aDirectory) {
  AssertIsOnIOThread();

  // Callers call this function without checking if the directory already
  // exists (idempotent usage). QM_OR_ELSE_WARN_IF is not used here since we
  // just want to log NS_ERROR_FILE_ALREADY_EXISTS result and not spam the
  // reports.
  QM_TRY_INSPECT(const auto& exists,
                 QM_OR_ELSE_LOG_VERBOSE_IF(
                     // Expression.
                     MOZ_TO_RESULT_INVOKE_MEMBER(aDirectory, Create,
                                                 nsIFile::DIRECTORY_TYPE, 0755,
                                                 /* aSkipAncestors = */ false)
                         .map([](Ok) { return false; }),
                     // Predicate.
                     IsSpecificError<NS_ERROR_FILE_ALREADY_EXISTS>,
                     // Fallback.
                     ErrToOk<true>));

  if (exists) {
    QM_TRY_INSPECT(const bool& isDirectory,
                   MOZ_TO_RESULT_INVOKE_MEMBER(aDirectory, IsDirectory));
    QM_TRY(OkIf(isDirectory), Err(NS_ERROR_UNEXPECTED));
  }

  return !exists;
}

void GetJarPrefix(bool aInIsolatedMozBrowser, nsACString& aJarPrefix) {
  aJarPrefix.Truncate();

  // Fallback.
  if (!aInIsolatedMozBrowser) {
    return;
  }

  // AppId is an unused b2g identifier. Let's set it to 0 all the time (see bug
  // 1320404).
  // aJarPrefix = appId + "+" + { 't', 'f' } + "+";
  aJarPrefix.AppendInt(0);  // TODO: this is the appId, to be removed.
  aJarPrefix.Append('+');
  aJarPrefix.Append(aInIsolatedMozBrowser ? 't' : 'f');
  aJarPrefix.Append('+');
}

// This method computes and returns our best guess for the temporary storage
// limit (in bytes), based on disk capacity.
Result<uint64_t, nsresult> GetTemporaryStorageLimit(nsIFile& aStorageDir) {
  // The fixed limit pref can be used to override temporary storage limit
  // calculation.
  if (StaticPrefs::dom_quotaManager_temporaryStorage_fixedLimit() >= 0) {
    return static_cast<uint64_t>(
               StaticPrefs::dom_quotaManager_temporaryStorage_fixedLimit()) *
           1024;
  }

  constexpr int64_t teraByte = (1024LL * 1024LL * 1024LL * 1024LL);
  constexpr int64_t maxAllowedCapacity = 8LL * teraByte;

  // Check for disk capacity of user's device on which storage directory lives.
  int64_t diskCapacity = maxAllowedCapacity;

  // Log error when default disk capacity is returned due to the error
  QM_WARNONLY_TRY(MOZ_TO_RESULT(aStorageDir.GetDiskCapacity(&diskCapacity)));

  MOZ_ASSERT(diskCapacity >= 0LL);

  // Allow temporary storage to consume up to 50% of disk capacity.
  int64_t capacityLimit = diskCapacity / 2LL;

  // If the disk capacity reported by the operating system is very
  // large and potentially incorrect due to hardware issues,
  // a hardcoded limit is supplied instead.
  QM_WARNONLY_TRY(
      OkIf(capacityLimit < maxAllowedCapacity),
      ([&capacityLimit](const auto&) { capacityLimit = maxAllowedCapacity; }));

  return capacityLimit;
}

bool IsOriginUnaccessed(const FullOriginMetadata& aFullOriginMetadata,
                        const int64_t aRecentTime) {
  if (aFullOriginMetadata.mLastAccessTime > aRecentTime) {
    return false;
  }

  return (aRecentTime - aFullOriginMetadata.mLastAccessTime) / PR_USEC_PER_SEC >
         StaticPrefs::dom_quotaManager_unaccessedForLongTimeThresholdSec();
}

}  // namespace

/*******************************************************************************
 * Exported functions
 ******************************************************************************/

void InitializeQuotaManager() {
  MOZ_ASSERT(XRE_IsParentProcess());
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(!gQuotaManagerInitialized);

  if (!QuotaManager::IsRunningGTests()) {
    // These services have to be started on the main thread currently.
    const nsCOMPtr<mozIStorageService> ss =
        do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID);
    QM_WARNONLY_TRY(OkIf(ss));

    RefPtr<net::ExtensionProtocolHandler> extensionProtocolHandler =
        net::ExtensionProtocolHandler::GetSingleton();
    QM_WARNONLY_TRY(MOZ_TO_RESULT(extensionProtocolHandler));
  }

  QM_WARNONLY_TRY(QM_TO_RESULT(QuotaManager::Initialize()));

#ifdef DEBUG
  gQuotaManagerInitialized = true;
#endif
}

void InitializeScopedLogExtraInfo() {
#ifdef QM_SCOPED_LOG_EXTRA_INFO_ENABLED
  ScopedLogExtraInfo::Initialize();
#endif
}

bool RecvShutdownQuotaManager() {
  AssertIsOnBackgroundThread();

  // If we are already in shutdown, don't call ShutdownInstance()
  // again and return true immediately. We shall see this incident
  // in Telemetry.
  // XXX todo: Make QM_TRY stacks thread-aware (Bug 1735124)
  // XXX todo: Active QM_TRY context for shutdown (Bug 1735170)
  QM_TRY(OkIf(!gShutdown), true);

  QuotaManager::ShutdownInstance();

  return true;
}

QuotaManager::Observer* QuotaManager::Observer::sInstance = nullptr;

// static
nsresult QuotaManager::Observer::Initialize() {
  MOZ_ASSERT(NS_IsMainThread());

  RefPtr<Observer> observer = new Observer();

  nsresult rv = observer->Init();
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  sInstance = observer;

  return NS_OK;
}

// static
nsIObserver* QuotaManager::Observer::GetInstance() {
  MOZ_ASSERT(NS_IsMainThread());

  return sInstance;
}

// static
void QuotaManager::Observer::ShutdownCompleted() {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(sInstance);

  sInstance->mShutdownComplete = true;
}

nsresult QuotaManager::Observer::Init() {
  MOZ_ASSERT(NS_IsMainThread());

  nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
  if (NS_WARN_IF(!obs)) {
    return NS_ERROR_FAILURE;
  }

  // XXX: Improve the way that we remove observer in failure cases.
  nsresult rv = obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = obs->AddObserver(this, kProfileDoChangeTopic, false);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
    return rv;
  }

  rv = obs->AddObserver(this, PROFILE_BEFORE_CHANGE_QM_OBSERVER_ID, false);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    obs->RemoveObserver(this, kProfileDoChangeTopic);
    obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
    return rv;
  }

  rv = obs->AddObserver(this, NS_WIDGET_WAKE_OBSERVER_TOPIC, false);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    obs->RemoveObserver(this, PROFILE_BEFORE_CHANGE_QM_OBSERVER_ID);
    obs->RemoveObserver(this, kProfileDoChangeTopic);
    obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
    return rv;
  }

  rv = obs->AddObserver(this, kPrivateBrowsingObserverTopic, false);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    obs->RemoveObserver(this, NS_WIDGET_WAKE_OBSERVER_TOPIC);
    obs->RemoveObserver(this, PROFILE_BEFORE_CHANGE_QM_OBSERVER_ID);
    obs->RemoveObserver(this, kProfileDoChangeTopic);
    obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
    return rv;
  }

  return NS_OK;
}

nsresult QuotaManager::Observer::Shutdown() {
  MOZ_ASSERT(NS_IsMainThread());

  nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
  if (NS_WARN_IF(!obs)) {
    return NS_ERROR_FAILURE;
  }

  MOZ_ALWAYS_SUCCEEDS(obs->RemoveObserver(this, kPrivateBrowsingObserverTopic));
  MOZ_ALWAYS_SUCCEEDS(obs->RemoveObserver(this, NS_WIDGET_WAKE_OBSERVER_TOPIC));
  MOZ_ALWAYS_SUCCEEDS(
      obs->RemoveObserver(this, PROFILE_BEFORE_CHANGE_QM_OBSERVER_ID));
  MOZ_ALWAYS_SUCCEEDS(obs->RemoveObserver(this, kProfileDoChangeTopic));
  MOZ_ALWAYS_SUCCEEDS(obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID));

  sInstance = nullptr;

  // In general, the instance will have died after the latter removal call, so
  // it's not safe to do anything after that point.
  // However, Shutdown is currently called from Observe which is called by the
  // Observer Service which holds a strong reference to the observer while the
  // Observe method is being called.

  return NS_OK;
}

NS_IMPL_ISUPPORTS(QuotaManager::Observer, nsIObserver)

NS_IMETHODIMP
QuotaManager::Observer::Observe(nsISupports* aSubject, const char* aTopic,
                                const char16_t* aData) {
  MOZ_ASSERT(NS_IsMainThread());

  nsresult rv;

  if (!strcmp(aTopic, kProfileDoChangeTopic)) {
    if (NS_WARN_IF(gBasePath)) {
      NS_WARNING(
          "profile-before-change-qm must precede repeated "
          "profile-do-change!");
      return NS_OK;
    }

    Telemetry::SetEventRecordingEnabled("dom.quota.try"_ns, true);

    gBasePath = new nsString();

    nsCOMPtr<nsIFile> baseDir;
    rv = NS_GetSpecialDirectory(NS_APP_INDEXEDDB_PARENT_DIR,
                                getter_AddRefs(baseDir));
    if (NS_FAILED(rv)) {
      rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
                                  getter_AddRefs(baseDir));
    }
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }

    rv = baseDir->GetPath(*gBasePath);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }

    gStorageName = new nsString();

    rv = Preferences::GetString("dom.quotaManager.storageName", *gStorageName);
    if (NS_FAILED(rv)) {
      *gStorageName = kStorageName;
    }

    gBuildId = new nsCString();

    nsCOMPtr<nsIPlatformInfo> platformInfo =
        do_GetService("@mozilla.org/xre/app-info;1");
    if (NS_WARN_IF(!platformInfo)) {
      return NS_ERROR_FAILURE;
    }

    rv = platformInfo->GetPlatformBuildID(*gBuildId);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }

    return NS_OK;
  }

  if (!strcmp(aTopic, PROFILE_BEFORE_CHANGE_QM_OBSERVER_ID)) {
    if (NS_WARN_IF(!gBasePath)) {
      NS_WARNING("profile-do-change must precede profile-before-change-qm!");
      return NS_OK;
    }

    // mPendingProfileChange is our re-entrancy guard (the nested event loop
    // below may cause re-entrancy).
    if (mPendingProfileChange) {
      return NS_OK;
    }

    AutoRestore<bool> pending(mPendingProfileChange);
    mPendingProfileChange = true;

    mShutdownComplete = false;

    PBackgroundChild* backgroundActor =
        BackgroundChild::GetOrCreateForCurrentThread();
    if (NS_WARN_IF(!backgroundActor)) {
      return NS_ERROR_FAILURE;
    }

    if (NS_WARN_IF(!backgroundActor->SendShutdownQuotaManager())) {
      return NS_ERROR_FAILURE;
    }

    MOZ_ALWAYS_TRUE(SpinEventLoopUntil(
        "QuotaManager::Observer::Observe profile-before-change-qm"_ns,
        [&]() { return mShutdownComplete; }));

    gBasePath = nullptr;

    gStorageName = nullptr;

    gBuildId = nullptr;

    Telemetry::SetEventRecordingEnabled("dom.quota.try"_ns, false);

    return NS_OK;
  }

  if (!strcmp(aTopic, kPrivateBrowsingObserverTopic)) {
    auto* const quotaManagerService = QuotaManagerService::GetOrCreate();
    if (NS_WARN_IF(!quotaManagerService)) {
      return NS_ERROR_FAILURE;
    }

    nsCOMPtr<nsIQuotaRequest> request;
    rv = quotaManagerService->ClearStoragesForPrivateBrowsing(
        nsGetterAddRefs(request));
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }

    return NS_OK;
  }

  if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) {
    rv = Shutdown();
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }

    return NS_OK;
  }

  if (!strcmp(aTopic, NS_WIDGET_WAKE_OBSERVER_TOPIC)) {
    gLastOSWake = TimeStamp::Now();

    return NS_OK;
  }

  NS_WARNING("Unknown observer topic!");
  return NS_OK;
}

/*******************************************************************************
 * Quota manager
 ******************************************************************************/

QuotaManager::QuotaManager(const nsAString& aBasePath,
                           const nsAString& aStorageName)
    : mQuotaMutex("QuotaManager.mQuotaMutex"),
      mBasePath(aBasePath),
      mStorageName(aStorageName),
      mTemporaryStorageUsage(0),
      mNextDirectoryLockId(0),
      mTemporaryStorageInitialized(false),
      mCacheUsable(false),
      mShuttingDownStorage(false) {
  AssertIsOnOwningThread();
  MOZ_ASSERT(!gInstance);
}

QuotaManager::~QuotaManager() {
  AssertIsOnOwningThread();
  MOZ_ASSERT(!gInstance || gInstance == this);
}

// static
nsresult QuotaManager::Initialize() {
  MOZ_ASSERT(NS_IsMainThread());

  nsresult rv = Observer::Initialize();
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

// static
Result<MovingNotNull<RefPtr<QuotaManager>>, nsresult>
QuotaManager::GetOrCreate() {
  AssertIsOnBackgroundThread();

  if (gInstance) {
    return WrapMovingNotNullUnchecked(RefPtr<QuotaManager>{gInstance});
  }

  QM_TRY(OkIf(gBasePath), Err(NS_ERROR_FAILURE), [](const auto&) {
    NS_WARNING(
        "Trying to create QuotaManager before profile-do-change! "
        "Forgot to call do_get_profile()?");
  });

  QM_TRY(OkIf(!IsShuttingDown()), Err(NS_ERROR_FAILURE), [](const auto&) {
    MOZ_ASSERT(false,
               "Trying to create QuotaManager after profile-before-change-qm!");
  });

  auto instance = MakeRefPtr<QuotaManager>(*gBasePath, *gStorageName);

  QM_TRY(MOZ_TO_RESULT(instance->Init()));

  gInstance = instance;

  // Do this before clients have a chance to acquire a directory lock for the
  // private repository.
  gInstance->ClearPrivateRepository();

  return WrapMovingNotNullUnchecked(std::move(instance));
}

Result<Ok, nsresult> QuotaManager::EnsureCreated() {
  AssertIsOnBackgroundThread();

  QM_TRY_RETURN(GetOrCreate().map([](const auto& res) { return Ok{}; }))
}

// static
QuotaManager* QuotaManager::Get() {
  // Does not return an owning reference.
  return gInstance;
}

// static
nsIObserver* QuotaManager::GetObserver() {
  MOZ_ASSERT(NS_IsMainThread());

  return Observer::GetInstance();
}

// static
bool QuotaManager::IsShuttingDown() { return gShutdown; }

// static
void QuotaManager::ShutdownInstance() {
  AssertIsOnBackgroundThread();

  if (gInstance) {
    gInstance->Shutdown();

    gInstance = nullptr;
  } else {
    // If we were never initialized, just set the flag to avoid late creation.
    gShutdown = true;
  }

  RefPtr<Runnable> runnable =
      NS_NewRunnableFunction("dom::quota::QuotaManager::ShutdownCompleted",
                             []() { Observer::ShutdownCompleted(); });
  MOZ_ASSERT(runnable);

  MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(runnable.forget()));
}

// static
void QuotaManager::Reset() {
  AssertIsOnBackgroundThread();
  MOZ_ASSERT(!gInstance);
  MOZ_ASSERT(gShutdown);

  gShutdown = false;
}

// static
bool QuotaManager::IsOSMetadata(const nsAString& aFileName) {
  return mozilla::dom::quota::IsOSMetadata(aFileName);
}

// static
bool QuotaManager::IsDotFile(const nsAString& aFileName) {
  return mozilla::dom::quota::IsDotFile(aFileName);
}

void QuotaManager::RegisterNormalOriginOp(
    NormalOriginOperationBase& aNormalOriginOp) {
  AssertIsOnBackgroundThread();

  if (!gNormalOriginOps) {
    gNormalOriginOps = new NormalOriginOpArray();
  }

  gNormalOriginOps->AppendElement(&aNormalOriginOp);
}

void QuotaManager::UnregisterNormalOriginOp(
    NormalOriginOperationBase& aNormalOriginOp) {
  AssertIsOnBackgroundThread();
  MOZ_ASSERT(gNormalOriginOps);

  gNormalOriginOps->RemoveElement(&aNormalOriginOp);

  if (gNormalOriginOps->IsEmpty()) {
    gNormalOriginOps = nullptr;
  }
}

void QuotaManager::RegisterDirectoryLock(DirectoryLockImpl& aLock) {
  AssertIsOnOwningThread();

  mDirectoryLocks.AppendElement(WrapNotNullUnchecked(&aLock));

  if (aLock.ShouldUpdateLockIdTable()) {
    MutexAutoLock lock(mQuotaMutex);

    MOZ_DIAGNOSTIC_ASSERT(!mDirectoryLockIdTable.Contains(aLock.Id()));
    mDirectoryLockIdTable.InsertOrUpdate(aLock.Id(),
                                         WrapNotNullUnchecked(&aLock));
  }

  if (aLock.ShouldUpdateLockTable()) {
    DirectoryLockTable& directoryLockTable =
        GetDirectoryLockTable(aLock.GetPersistenceType());

    // XXX It seems that the contents of the array are never actually used, we
    // just use that like an inefficient use counter. Can't we just change
    // DirectoryLockTable to a nsTHashMap<nsCStringHashKey, uint32_t>?
    directoryLockTable
        .LookupOrInsertWith(
            aLock.Origin(),
            [this, &aLock] {
              if (!IsShuttingDown()) {
                UpdateOriginAccessTime(aLock.GetPersistenceType(),
                                       aLock.OriginMetadata());
              }
              return MakeUnique<nsTArray<NotNull<DirectoryLockImpl*>>>();
            })
        ->AppendElement(WrapNotNullUnchecked(&aLock));
  }

  aLock.SetRegistered(true);
}

void QuotaManager::UnregisterDirectoryLock(DirectoryLockImpl& aLock) {
  AssertIsOnOwningThread();

  MOZ_ALWAYS_TRUE(mDirectoryLocks.RemoveElement(&aLock));

  if (aLock.ShouldUpdateLockIdTable()) {
    MutexAutoLock lock(mQuotaMutex);

    MOZ_DIAGNOSTIC_ASSERT(mDirectoryLockIdTable.Contains(aLock.Id()));
    mDirectoryLockIdTable.Remove(aLock.Id());
  }

  if (aLock.ShouldUpdateLockTable()) {
    DirectoryLockTable& directoryLockTable =
        GetDirectoryLockTable(aLock.GetPersistenceType());

    nsTArray<NotNull<DirectoryLockImpl*>>* array;
    MOZ_ALWAYS_TRUE(directoryLockTable.Get(aLock.Origin(), &array));

    MOZ_ALWAYS_TRUE(array->RemoveElement(&aLock));
    if (array->IsEmpty()) {
      directoryLockTable.Remove(aLock.Origin());

      if (!IsShuttingDown()) {
        UpdateOriginAccessTime(aLock.GetPersistenceType(),
                               aLock.OriginMetadata());
      }
    }
  }

  aLock.SetRegistered(false);
}

void QuotaManager::AddPendingDirectoryLock(DirectoryLockImpl& aLock) {
  AssertIsOnOwningThread();

  mPendingDirectoryLocks.AppendElement(&aLock);
}

void QuotaManager::RemovePendingDirectoryLock(DirectoryLockImpl& aLock) {
  AssertIsOnOwningThread();

  MOZ_ALWAYS_TRUE(mPendingDirectoryLocks.RemoveElement(&aLock));
}

uint64_t QuotaManager::CollectOriginsForEviction(
    uint64_t aMinSizeToBeFreed, nsTArray<RefPtr<OriginDirectoryLock>>& aLocks) {
  AssertIsOnOwningThread();
  MOZ_ASSERT(aLocks.IsEmpty());

  // XXX This looks as if this could/should also use CollectLRUOriginInfosUntil,
  // or maybe a generalization if that.

  struct MOZ_STACK_CLASS Helper final {
    static void GetInactiveOriginInfos(
        const nsTArray<NotNull<RefPtr<OriginInfo>>>& aOriginInfos,
        const nsTArray<NotNull<const DirectoryLockImpl*>>& aLocks,
        OriginInfosFlatTraversable& aInactiveOriginInfos) {
      for (const auto& originInfo : aOriginInfos) {
        MOZ_ASSERT(originInfo->mGroupInfo->mPersistenceType !=
                   PERSISTENCE_TYPE_PERSISTENT);

        if (originInfo->LockedPersisted()) {
          continue;
        }

        // Never evict PERSISTENCE_TYPE_DEFAULT data associated to a
        // moz-extension origin, unlike websites (which may more likely using
        // the local data as a cache but still able to retrieve the same data
        // from the server side) extensions do not have the same data stored
        // anywhere else and evicting the data would result into potential data
        // loss for the users.
        //
        // Also, unlike a website the extensions are explicitly installed and
        // uninstalled by the user and all data associated to the extension
        // principal will be completely removed once the addon is uninstalled.
        if (originInfo->mGroupInfo->mPersistenceType !=
                PERSISTENCE_TYPE_TEMPORARY &&
            originInfo->IsExtensionOrigin()) {
          continue;
        }

        const auto originScope = OriginScope::FromOrigin(originInfo->mOrigin);

        const bool match =
            std::any_of(aLocks.begin(), aLocks.end(),
                        [&originScope](const DirectoryLockImpl* const lock) {
                          return originScope.Matches(lock->GetOriginScope());
                        });

        if (!match) {
          MOZ_ASSERT(!originInfo->mCanonicalQuotaObjects.Count(),
                     "Inactive origin shouldn't have open files!");
          aInactiveOriginInfos.InsertElementSorted(
              originInfo, OriginInfoAccessTimeComparator());
        }
      }
    }
  };

  // Split locks into separate arrays and filter out locks for persistent
  // storage, they can't block us.
  auto [temporaryStorageLocks, defaultStorageLocks,
        privateStorageLocks] = [this] {
    nsTArray<NotNull<const DirectoryLockImpl*>> temporaryStorageLocks;
    nsTArray<NotNull<const DirectoryLockImpl*>> defaultStorageLocks;
    nsTArray<NotNull<const DirectoryLockImpl*>> privateStorageLocks;

    for (NotNull<const DirectoryLockImpl*> const lock : mDirectoryLocks) {
      const Nullable<PersistenceType>& persistenceType =
          lock->NullablePersistenceType();

      if (persistenceType.IsNull()) {
        temporaryStorageLocks.AppendElement(lock);
        defaultStorageLocks.AppendElement(lock);
      } else if (persistenceType.Value() == PERSISTENCE_TYPE_TEMPORARY) {
        temporaryStorageLocks.AppendElement(lock);
      } else if (persistenceType.Value() == PERSISTENCE_TYPE_DEFAULT) {
        defaultStorageLocks.AppendElement(lock);
      } else if (persistenceType.Value() == PERSISTENCE_TYPE_PRIVATE) {
        privateStorageLocks.AppendElement(lock);
      } else {
        MOZ_ASSERT(persistenceType.Value() == PERSISTENCE_TYPE_PERSISTENT);

        // Do nothing here, persistent origins don't need to be collected ever.
      }
    }

    return std::make_tuple(std::move(temporaryStorageLocks),
                           std::move(defaultStorageLocks),
                           std::move(privateStorageLocks));
  }();

  // Enumerate and process inactive origins. This must be protected by the
  // mutex.
  MutexAutoLock lock(mQuotaMutex);

  const auto [inactiveOrigins, sizeToBeFreed] =
      [this, &temporaryStorageLocks = temporaryStorageLocks,
       &defaultStorageLocks = defaultStorageLocks,
       &privateStorageLocks = privateStorageLocks, aMinSizeToBeFreed] {
        nsTArray<NotNull<RefPtr<const OriginInfo>>> inactiveOrigins;
        for (const auto& entry : mGroupInfoPairs) {
          const auto& pair = entry.GetData();

          MOZ_ASSERT(!entry.GetKey().IsEmpty());
          MOZ_ASSERT(pair);

          RefPtr<GroupInfo> groupInfo =
              pair->LockedGetGroupInfo(PERSISTENCE_TYPE_TEMPORARY);
          if (groupInfo) {
            Helper::GetInactiveOriginInfos(groupInfo->mOriginInfos,
                                           temporaryStorageLocks,
                                           inactiveOrigins);
          }

          groupInfo = pair->LockedGetGroupInfo(PERSISTENCE_TYPE_DEFAULT);
          if (groupInfo) {
            Helper::GetInactiveOriginInfos(
                groupInfo->mOriginInfos, defaultStorageLocks, inactiveOrigins);
          }

          groupInfo = pair->LockedGetGroupInfo(PERSISTENCE_TYPE_PRIVATE);
          if (groupInfo) {
            Helper::GetInactiveOriginInfos(
                groupInfo->mOriginInfos, privateStorageLocks, inactiveOrigins);
          }
        }

#ifdef DEBUG
        // Make sure the array is sorted correctly.
        const bool inactiveOriginsSorted =
            std::is_sorted(inactiveOrigins.cbegin(), inactiveOrigins.cend(),
                           [](const auto& lhs, const auto& rhs) {
                             return lhs->mAccessTime < rhs->mAccessTime;
                           });
        MOZ_ASSERT(inactiveOriginsSorted);
#endif

        // Create a list of inactive and the least recently used origins
        // whose aggregate size is greater or equals the minimal size to be
        // freed.
        uint64_t sizeToBeFreed = 0;
        for (uint32_t count = inactiveOrigins.Length(), index = 0;
             index < count; index++) {
          if (sizeToBeFreed >= aMinSizeToBeFreed) {
            inactiveOrigins.TruncateLength(index);
            break;
          }

          sizeToBeFreed += inactiveOrigins[index]->LockedUsage();
        }

        return std::pair(std::move(inactiveOrigins), sizeToBeFreed);
      }();

  if (sizeToBeFreed >= aMinSizeToBeFreed) {
    // Success, add directory locks for these origins, so any other
    // operations for them will be delayed (until origin eviction is finalized).

    for (const auto& originInfo : inactiveOrigins) {
      auto lock = DirectoryLockImpl::CreateForEviction(
          WrapNotNullUnchecked(this), originInfo->mGroupInfo->mPersistenceType,
          originInfo->FlattenToOriginMetadata());

      lock->AcquireImmediately();

      aLocks.AppendElement(lock.forget());
    }

    return sizeToBeFreed;
  }

  return 0;
}

nsresult QuotaManager::Init() {
  AssertIsOnOwningThread();

#ifdef XP_WIN
  CacheUseDOSDevicePathSyntaxPrefValue();
#endif

  QM_TRY_INSPECT(const auto& baseDir, QM_NewLocalFile(mBasePath));

  QM_TRY_UNWRAP(
      do_Init(mIndexedDBPath),
      GetPathForStorage(*baseDir, nsLiteralString(INDEXEDDB_DIRECTORY_NAME)));

  QM_TRY(MOZ_TO_RESULT(baseDir->Append(mStorageName)));

  QM_TRY_UNWRAP(do_Init(mStoragePath),
                MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, baseDir, GetPath));

  QM_TRY_UNWRAP(
      do_Init(mStorageArchivesPath),
      GetPathForStorage(*baseDir, nsLiteralString(ARCHIVES_DIRECTORY_NAME)));

  QM_TRY_UNWRAP(
      do_Init(mPermanentStoragePath),
      GetPathForStorage(*baseDir, nsLiteralString(PERMANENT_DIRECTORY_NAME)));

  QM_TRY_UNWRAP(
      do_Init(mTemporaryStoragePath),
      GetPathForStorage(*baseDir, nsLiteralString(TEMPORARY_DIRECTORY_NAME)));

  QM_TRY_UNWRAP(
      do_Init(mDefaultStoragePath),
      GetPathForStorage(*baseDir, nsLiteralString(DEFAULT_DIRECTORY_NAME)));

  QM_TRY_UNWRAP(do_Init(mPrivateStoragePath),
                GetPathForStorage(
                    *baseDir, nsLiteralString(DEFAULT_PRIVATE_DIRECTORY_NAME)));

  QM_TRY_UNWRAP(do_Init(mIOThread),
                MOZ_TO_RESULT_INVOKE_TYPED(
                    nsCOMPtr<nsIThread>, MOZ_SELECT_OVERLOAD(NS_NewNamedThread),
                    "QuotaManager IO"));

  static_assert(Client::IDB == 0 && Client::DOMCACHE == 1 && Client::SDB == 2 &&
                    Client::FILESYSTEM == 3 && Client::LS == 4 &&
                    Client::TYPE_MAX == 5,
                "Fix the registration!");

  // Register clients.
  auto clients = decltype(mClients)::ValueType{};
  clients.AppendElement(indexedDB::CreateQuotaClient());
  clients.AppendElement(cache::CreateQuotaClient());
  clients.AppendElement(simpledb::CreateQuotaClient());
  clients.AppendElement(fs::CreateQuotaClient());
  if (NextGenLocalStorageEnabled()) {
    clients.AppendElement(localstorage::CreateQuotaClient());
  } else {
    clients.SetLength(Client::TypeMax());
  }

  mClients.init(std::move(clients));

  MOZ_ASSERT(mClients->Capacity() == Client::TYPE_MAX,
             "Should be using an auto array with correct capacity!");

  mAllClientTypes.init(ClientTypesArray{
      Client::Type::IDB, Client::Type::DOMCACHE, Client::Type::SDB,
      Client::Type::FILESYSTEM, Client::Type::LS});
  mAllClientTypesExceptLS.init(
      ClientTypesArray{Client::Type::IDB, Client::Type::DOMCACHE,
                       Client::Type::SDB, Client::Type::FILESYSTEM});

  return NS_OK;
}

// static
void QuotaManager::MaybeRecordQuotaClientShutdownStep(
    const Client::Type aClientType, const nsACString& aStepDescription) {
  // Callable on any thread.

  auto* const quotaManager = QuotaManager::Get();
  MOZ_DIAGNOSTIC_ASSERT(quotaManager);

  if (quotaManager->IsShuttingDown()) {
    quotaManager->RecordShutdownStep(Some(aClientType), aStepDescription);
  }
}

// static
void QuotaManager::SafeMaybeRecordQuotaClientShutdownStep(
    const Client::Type aClientType, const nsACString& aStepDescription) {
  // Callable on any thread.

  auto* const quotaManager = QuotaManager::Get();

  if (quotaManager && quotaManager->IsShuttingDown()) {
    quotaManager->RecordShutdownStep(Some(aClientType), aStepDescription);
  }
}

void QuotaManager::RecordQuotaManagerShutdownStep(
    const nsACString& aStepDescription) {
  // Callable on any thread.
  MOZ_ASSERT(IsShuttingDown());

  RecordShutdownStep(Nothing{}, aStepDescription);
}

void QuotaManager::MaybeRecordQuotaManagerShutdownStep(
    const nsACString& aStepDescription) {
  // Callable on any thread.

  if (IsShuttingDown()) {
    RecordQuotaManagerShutdownStep(aStepDescription);
  }
}

void QuotaManager::RecordShutdownStep(const Maybe<Client::Type> aClientType,
                                      const nsACString& aStepDescription) {
  MOZ_ASSERT(IsShuttingDown());

  const TimeDuration elapsedSinceShutdownStart =
      TimeStamp::NowLoRes() - *mShutdownStartedAt;

  const auto stepString =
      nsPrintfCString("%fs: %s", elapsedSinceShutdownStart.ToSeconds(),
                      nsPromiseFlatCString(aStepDescription).get());

  if (aClientType) {
    AssertIsOnBackgroundThread();

    mShutdownSteps[*aClientType].Append(stepString + "\n"_ns);
  } else {
    // Callable on any thread.
    MutexAutoLock lock(mQuotaMutex);

    mQuotaManagerShutdownSteps.Append(stepString + "\n"_ns);
  }

#ifdef DEBUG
  // XXX Probably this isn't the mechanism that should be used here.

  NS_DebugBreak(
      NS_DEBUG_WARNING,
      nsAutoCString(aClientType ? Client::TypeToText(*aClientType)
                                : "quota manager"_ns + " shutdown step"_ns)
          .get(),
      stepString.get(), __FILE__, __LINE__);
#endif
}

void QuotaManager::Shutdown() {
  AssertIsOnOwningThread();
  MOZ_DIAGNOSTIC_ASSERT(!gShutdown);

  // Define some local helper functions

  auto flagShutdownStarted = [this]() {
    mShutdownStartedAt.init(TimeStamp::NowLoRes());

    // Setting this flag prevents the service from being recreated and prevents
    // further storages from being created.
    gShutdown = true;
  };

  nsCOMPtr<nsITimer> crashBrowserTimer;

  auto crashBrowserTimerCallback = [](nsITimer* aTimer, void* aClosure) {
    auto* const quotaManager = static_cast<QuotaManager*>(aClosure);

    nsCString annotation;

    for (Client::Type type : quotaManager->AllClientTypes()) {
      auto& quotaClient = *(*quotaManager->mClients)[type];

      if (!quotaClient.IsShutdownCompleted()) {
        annotation.AppendPrintf("%s: %s\nIntermediate steps:\n%s\n\n",
                                Client::TypeToText(type).get(),
                                quotaClient.GetShutdownStatus().get(),
                                quotaManager->mShutdownSteps[type].get());
      }
    }

    if (gNormalOriginOps) {
      annotation.AppendPrintf("QM: %zu normal origin ops pending\n",
                              gNormalOriginOps->Length());
#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY
      for (const auto& op : *gNormalOriginOps) {
        nsCString name;
        op->GetName(name);
        annotation.AppendPrintf("Op: %s pending\n", name.get());
      }
#endif
    }
    {
      MutexAutoLock lock(quotaManager->mQuotaMutex);

      annotation.AppendPrintf("Intermediate steps:\n%s\n",
                              quotaManager->mQuotaManagerShutdownSteps.get());
    }

    CrashReporter::AnnotateCrashReport(
        CrashReporter::Annotation::QuotaManagerShutdownTimeout, annotation);

    MOZ_CRASH("Quota manager shutdown timed out");
  };

  auto startCrashBrowserTimer = [&]() {
    crashBrowserTimer = NS_NewTimer();
    MOZ_ASSERT(crashBrowserTimer);
    if (crashBrowserTimer) {
      RecordQuotaManagerShutdownStep("startCrashBrowserTimer"_ns);
      MOZ_ALWAYS_SUCCEEDS(crashBrowserTimer->InitWithNamedFuncCallback(
          crashBrowserTimerCallback, this, SHUTDOWN_CRASH_BROWSER_TIMEOUT_MS,
          nsITimer::TYPE_ONE_SHOT,
          "quota::QuotaManager::Shutdown::crashBrowserTimer"));
    }
  };

  auto stopCrashBrowserTimer = [&]() {
    if (crashBrowserTimer) {
      RecordQuotaManagerShutdownStep("stopCrashBrowserTimer"_ns);
      QM_WARNONLY_TRY(QM_TO_RESULT(crashBrowserTimer->Cancel()));
    }
  };

  auto initiateShutdownWorkThreads = [this]() {
    RecordQuotaManagerShutdownStep("initiateShutdownWorkThreads"_ns);
    bool needsToWait = false;
    for (Client::Type type : AllClientTypes()) {
      // Clients are supposed to also AbortAllOperations from this point on
      // to speed up shutdown, if possible. Thus pending operations
      // might not be executed anymore.
      needsToWait |= (*mClients)[type]->InitiateShutdownWorkThreads();
    }

    return needsToWait;
  };

  nsCOMPtr<nsITimer> killActorsTimer;

  auto killActorsTimerCallback = [](nsITimer* aTimer, void* aClosure) {
    auto* const quotaManager = static_cast<QuotaManager*>(aClosure);

    quotaManager->RecordQuotaManagerShutdownStep("killActorsTimerCallback"_ns);

    // XXX: This abort is a workaround to unblock shutdown, which
    // ought to be removed by bug 1682326. We probably need more
    // checks to immediately abort new operations during
    // shutdown.
    quotaManager->GetClient(Client::IDB)->AbortAllOperations();

    for (Client::Type type : quotaManager->AllClientTypes()) {
      quotaManager->GetClient(type)->ForceKillActors();
    }
  };

  auto startKillActorsTimer = [&]() {
    killActorsTimer = NS_NewTimer();
    MOZ_ASSERT(killActorsTimer);
    if (killActorsTimer) {
      RecordQuotaManagerShutdownStep("startKillActorsTimer"_ns);
      MOZ_ALWAYS_SUCCEEDS(killActorsTimer->InitWithNamedFuncCallback(
          killActorsTimerCallback, this, SHUTDOWN_KILL_ACTORS_TIMEOUT_MS,
          nsITimer::TYPE_ONE_SHOT,
          "quota::QuotaManager::Shutdown::killActorsTimer"));
    }
  };

  auto stopKillActorsTimer = [&]() {
    if (killActorsTimer) {
      RecordQuotaManagerShutdownStep("stopKillActorsTimer"_ns);
      QM_WARNONLY_TRY(QM_TO_RESULT(killActorsTimer->Cancel()));
    }
  };

  auto isAllClientsShutdownComplete = [this] {
    return std::all_of(AllClientTypes().cbegin(), AllClientTypes().cend(),
                       [&self = *this](const auto type) {
                         return (*self.mClients)[type]->IsShutdownCompleted();
                       });
  };

  auto shutdownAndJoinWorkThreads = [this]() {
    RecordQuotaManagerShutdownStep("shutdownAndJoinWorkThreads"_ns);
    for (Client::Type type : AllClientTypes()) {
      (*mClients)[type]->FinalizeShutdownWorkThreads();
    }
  };

  auto shutdownAndJoinIOThread = [this]() {
    RecordQuotaManagerShutdownStep("shutdownAndJoinIOThread"_ns);

    // Make sure to join with our IO thread.
    QM_WARNONLY_TRY(QM_TO_RESULT((*mIOThread)->Shutdown()));
  };

  auto invalidatePendingDirectoryLocks = [this]() {
    RecordQuotaManagerShutdownStep("invalidatePendingDirectoryLocks"_ns);
    for (RefPtr<DirectoryLockImpl>& lock : mPendingDirectoryLocks) {
      lock->Invalidate();
    }
  };

  // Body of the function
  ScopedLogExtraInfo scope{ScopedLogExtraInfo::kTagContext,
                           "dom::quota::QuotaManager::Shutdown"_ns};

  // This must be called before `flagShutdownStarted`, it would fail otherwise.
  // `ShutdownStorageOp` needs to acquire an exclusive directory lock over
  // entire <profile>/storage which will abort any existing operations and wait
  // for all existing directory locks to be released. So the shutdown operation
  // will effectively run after all existing operations.
  // We don't need to use the returned promise here because `ShutdownStorage`
  // registers `ShudownStorageOp` in `gNormalOriginOps`.
  ShutdownStorage();

  flagShutdownStarted();

  startCrashBrowserTimer();

  // XXX: StopIdleMaintenance now just notifies all clients to abort any
  // maintenance work.
  // This could be done as part of QuotaClient::AbortAllOperations.
  StopIdleMaintenance();

  // XXX In theory, we could simplify the code below (and also the `Client`
  // interface) by removing the `initiateShutdownWorkThreads` and
  // `isAllClientsShutdownComplete` calls because it should be sufficient
  // to rely on `ShutdownStorage` to abort all existing operations and to
  // wait for all existing directory locks to be released as well.

  const bool needsToWait =
      initiateShutdownWorkThreads() || static_cast<bool>(gNormalOriginOps);

  // If any clients cannot shutdown immediately, spin the event loop while we
  // wait on all the threads to close.
  if (needsToWait) {
    startKillActorsTimer();

    MOZ_ALWAYS_TRUE(SpinEventLoopUntil(
        "QuotaManager::Shutdown"_ns, [isAllClientsShutdownComplete]() {
          return !gNormalOriginOps && isAllClientsShutdownComplete();
        }));

    stopKillActorsTimer();
  }

  shutdownAndJoinWorkThreads();

  shutdownAndJoinIOThread();

  invalidatePendingDirectoryLocks();

  stopCrashBrowserTimer();
}

void QuotaManager::InitQuotaForOrigin(
    const FullOriginMetadata& aFullOriginMetadata,
    const ClientUsageArray& aClientUsages, uint64_t aUsageBytes) {
  AssertIsOnIOThread();
  MOZ_ASSERT(IsBestEffortPersistenceType(aFullOriginMetadata.mPersistenceType));

  MutexAutoLock lock(mQuotaMutex);

  RefPtr<GroupInfo> groupInfo = LockedGetOrCreateGroupInfo(
      aFullOriginMetadata.mPersistenceType, aFullOriginMetadata.mSuffix,
      aFullOriginMetadata.mGroup);

  groupInfo->LockedAddOriginInfo(MakeNotNull<RefPtr<OriginInfo>>(
      groupInfo, aFullOriginMetadata.mOrigin,
      aFullOriginMetadata.mStorageOrigin, aFullOriginMetadata.mIsPrivate,
      aClientUsages, aUsageBytes, aFullOriginMetadata.mLastAccessTime,
      aFullOriginMetadata.mPersisted,
      /* aDirectoryExists */ true));
}

void QuotaManager::EnsureQuotaForOrigin(const OriginMetadata& aOriginMetadata) {
  AssertIsOnIOThread();
  MOZ_ASSERT(IsBestEffortPersistenceType(aOriginMetadata.mPersistenceType));

  MutexAutoLock lock(mQuotaMutex);

  RefPtr<GroupInfo> groupInfo = LockedGetOrCreateGroupInfo(
      aOriginMetadata.mPersistenceType, aOriginMetadata.mSuffix,
      aOriginMetadata.mGroup);

  RefPtr<OriginInfo> originInfo =
      groupInfo->LockedGetOriginInfo(aOriginMetadata.mOrigin);
  if (!originInfo) {
    groupInfo->LockedAddOriginInfo(MakeNotNull<RefPtr<OriginInfo>>(
        groupInfo, aOriginMetadata.mOrigin, aOriginMetadata.mStorageOrigin,
        aOriginMetadata.mIsPrivate, ClientUsageArray(),
        /* aUsageBytes */ 0,
        /* aAccessTime */ PR_Now(), /* aPersisted */ false,
        /* aDirectoryExists */ false));
  }
}

int64_t QuotaManager::NoteOriginDirectoryCreated(
    const OriginMetadata& aOriginMetadata, bool aPersisted) {
  AssertIsOnIOThread();
  MOZ_ASSERT(IsBestEffortPersistenceType(aOriginMetadata.mPersistenceType));

  int64_t timestamp;

  MutexAutoLock lock(mQuotaMutex);

  RefPtr<GroupInfo> groupInfo = LockedGetOrCreateGroupInfo(
      aOriginMetadata.mPersistenceType, aOriginMetadata.mSuffix,
      aOriginMetadata.mGroup);

  RefPtr<OriginInfo> originInfo =
      groupInfo->LockedGetOriginInfo(aOriginMetadata.mOrigin);
  if (originInfo) {
    timestamp = originInfo->LockedAccessTime();
    originInfo->mPersisted = aPersisted;
    originInfo->mDirectoryExists = true;
  } else {
    timestamp = PR_Now();
    groupInfo->LockedAddOriginInfo(MakeNotNull<RefPtr<OriginInfo>>(
        groupInfo, aOriginMetadata.mOrigin, aOriginMetadata.mStorageOrigin,
        aOriginMetadata.mIsPrivate, ClientUsageArray(),
        /* aUsageBytes */ 0,
        /* aAccessTime */ timestamp, aPersisted, /* aDirectoryExists */ true));
  }

  return timestamp;
}

void QuotaManager::DecreaseUsageForClient(const ClientMetadata& aClientMetadata,
                                          int64_t aSize) {
  MOZ_ASSERT(!NS_IsMainThread());
  MOZ_ASSERT(IsBestEffortPersistenceType(aClientMetadata.mPersistenceType));

  MutexAutoLock lock(mQuotaMutex);

  GroupInfoPair* pair;
  if (!mGroupInfoPairs.Get(aClientMetadata.mGroup, &pair)) {
    return;
  }

  RefPtr<GroupInfo> groupInfo =
      pair->LockedGetGroupInfo(aClientMetadata.mPersistenceType);
  if (!groupInfo) {
    return;
  }

  RefPtr<OriginInfo> originInfo =
      groupInfo->LockedGetOriginInfo(aClientMetadata.mOrigin);
  if (originInfo) {
    originInfo->LockedDecreaseUsage(aClientMetadata.mClientType, aSize);
  }
}

void QuotaManager::ResetUsageForClient(const ClientMetadata& aClientMetadata) {
  MOZ_ASSERT(!NS_IsMainThread());
  MOZ_ASSERT(IsBestEffortPersistenceType(aClientMetadata.mPersistenceType));

  MutexAutoLock lock(mQuotaMutex);

  GroupInfoPair* pair;
  if (!mGroupInfoPairs.Get(aClientMetadata.mGroup, &pair)) {
    return;
  }

  RefPtr<GroupInfo> groupInfo =
      pair->LockedGetGroupInfo(aClientMetadata.mPersistenceType);
  if (!groupInfo) {
    return;
  }

  RefPtr<OriginInfo> originInfo =
      groupInfo->LockedGetOriginInfo(aClientMetadata.mOrigin);
  if (originInfo) {
    originInfo->LockedResetUsageForClient(aClientMetadata.mClientType);
  }
}

UsageInfo QuotaManager::GetUsageForClient(PersistenceType aPersistenceType,
                                          const OriginMetadata& aOriginMetadata,
                                          Client::Type aClientType) {
  MOZ_ASSERT(!NS_IsMainThread());
  MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT);

  MutexAutoLock lock(mQuotaMutex);

  GroupInfoPair* pair;
  if (!mGroupInfoPairs.Get(aOriginMetadata.mGroup, &pair)) {
    return UsageInfo{};
  }

  RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(aPersistenceType);
  if (!groupInfo) {
    return UsageInfo{};
  }

  RefPtr<OriginInfo> originInfo =
      groupInfo->LockedGetOriginInfo(aOriginMetadata.mOrigin);
  if (!originInfo) {
    return UsageInfo{};
  }

  return originInfo->LockedGetUsageForClient(aClientType);
}

void QuotaManager::UpdateOriginAccessTime(
    PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata) {
  AssertIsOnOwningThread();
  MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT);
  MOZ_ASSERT(aOriginMetadata.mPersistenceType == aPersistenceType);
  MOZ_ASSERT(!IsShuttingDown());

  MutexAutoLock lock(mQuotaMutex);

  GroupInfoPair* pair;
  if (!mGroupInfoPairs.Get(aOriginMetadata.mGroup, &pair)) {
    return;
  }

  RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(aPersistenceType);
  if (!groupInfo) {
    return;
  }

  RefPtr<OriginInfo> originInfo =
      groupInfo->LockedGetOriginInfo(aOriginMetadata.mOrigin);
  if (originInfo) {
    int64_t timestamp = PR_Now();
    originInfo->LockedUpdateAccessTime(timestamp);

    MutexAutoUnlock autoUnlock(mQuotaMutex);

    auto op = CreateSaveOriginAccessTimeOp(aOriginMetadata, timestamp);

    RegisterNormalOriginOp(*op);

    op->RunImmediately();
  }
}

void QuotaManager::RemoveQuota() {
  AssertIsOnIOThread();

  MutexAutoLock lock(mQuotaMutex);

  for (const auto& entry : mGroupInfoPairs) {
    const auto& pair = entry.GetData();

    MOZ_ASSERT(!entry.GetKey().IsEmpty());
    MOZ_ASSERT(pair);

    RefPtr<GroupInfo> groupInfo =
        pair->LockedGetGroupInfo(PERSISTENCE_TYPE_TEMPORARY);
    if (groupInfo) {
      groupInfo->LockedRemoveOriginInfos();
    }

    groupInfo = pair->LockedGetGroupInfo(PERSISTENCE_TYPE_DEFAULT);
    if (groupInfo) {
      groupInfo->LockedRemoveOriginInfos();
    }

    groupInfo = pair->LockedGetGroupInfo(PERSISTENCE_TYPE_PRIVATE);
    if (groupInfo) {
      groupInfo->LockedRemoveOriginInfos();
    }
  }

  mGroupInfoPairs.Clear();

  MOZ_ASSERT(mTemporaryStorageUsage == 0, "Should be zero!");
}

nsresult QuotaManager::LoadQuota() {
  AssertIsOnIOThread();
  MOZ_ASSERT(mStorageConnection);
  MOZ_ASSERT(!mTemporaryStorageInitialized);

  // A list of all unaccessed default or temporary origins.
  nsTArray<FullOriginMetadata> unaccessedOrigins;

  auto MaybeCollectUnaccessedOrigin =
      [loadQuotaInfoStartTime = PR_Now(),
       &unaccessedOrigins](auto& fullOriginMetadata) {
        if (IsOriginUnaccessed(fullOriginMetadata, loadQuotaInfoStartTime)) {
          unaccessedOrigins.AppendElement(std::move(fullOriginMetadata));
        }
      };

  auto recordQuotaInfoLoadTimeHelper =
      MakeRefPtr<RecordQuotaInfoLoadTimeHelper>();

  const auto startTime = recordQuotaInfoLoadTimeHelper->Start();

  auto LoadQuotaFromCache = [&]() -> nsresult {
    QM_TRY_INSPECT(
        const auto& stmt,
        MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
            nsCOMPtr<mozIStorageStatement>, mStorageConnection, CreateStatement,
            "SELECT repository_id, suffix, group_, "
            "origin, client_usages, usage, "
            "last_access_time, accessed, persisted "
            "FROM origin"_ns));

    auto autoRemoveQuota = MakeScopeExit([&] {
      RemoveQuota();
      unaccessedOrigins.Clear();
    });

    QM_TRY(quota::CollectWhileHasResult(
        *stmt,
        [this,
         &MaybeCollectUnaccessedOrigin](auto& stmt) -> Result<Ok, nsresult> {
          QM_TRY_INSPECT(const int32_t& repositoryId,
                         MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt32, 0));

          const auto maybePersistenceType =
              PersistenceTypeFromInt32(repositoryId, fallible);
          QM_TRY(OkIf(maybePersistenceType.isSome()), Err(NS_ERROR_FAILURE));

          FullOriginMetadata fullOriginMetadata;

          fullOriginMetadata.mPersistenceType = maybePersistenceType.value();

          QM_TRY_UNWRAP(fullOriginMetadata.mSuffix,
                        MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, stmt,
                                                          GetUTF8String, 1));

          QM_TRY_UNWRAP(fullOriginMetadata.mGroup,
                        MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, stmt,
                                                          GetUTF8String, 2));

          QM_TRY_UNWRAP(fullOriginMetadata.mOrigin,
                        MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, stmt,
                                                          GetUTF8String, 3));

          fullOriginMetadata.mStorageOrigin = fullOriginMetadata.mOrigin;

          fullOriginMetadata.mIsPrivate = false;

          QM_TRY_INSPECT(const auto& clientUsagesText,
                         MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, stmt,
                                                           GetUTF8String, 4));

          ClientUsageArray clientUsages;
          QM_TRY(MOZ_TO_RESULT(clientUsages.Deserialize(clientUsagesText)));

          QM_TRY_INSPECT(const int64_t& usage,
                         MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt64, 5));
          QM_TRY_UNWRAP(fullOriginMetadata.mLastAccessTime,
                        MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt64, 6));
          QM_TRY_INSPECT(const int64_t& accessed,
                         MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt32, 7));
          QM_TRY_UNWRAP(fullOriginMetadata.mPersisted,
                        MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt32, 8));

          QM_TRY_INSPECT(const bool& groupUpdated,
                         MaybeUpdateGroupForOrigin(fullOriginMetadata));

          Unused << groupUpdated;

          QM_TRY_INSPECT(
              const bool& lastAccessTimeUpdated,
              MaybeUpdateLastAccessTimeForOrigin(fullOriginMetadata));

          Unused << lastAccessTimeUpdated;

          // We don't need to update the .metadata-v2 file on disk here,
          // EnsureTemporaryOriginIsInitialized is responsible for doing that.
          // We just need to use correct group and last access time before
          // initializing quota for the given origin. (Note that calling
          // LoadFullOriginMetadataWithRestore below might update the group in
          // the metadata file, but only as a side-effect. The actual place we
          // ensure consistency is in EnsureTemporaryOriginIsInitialized.)

          if (accessed) {
            QM_TRY_INSPECT(const auto& directory,
                           GetOriginDirectory(fullOriginMetadata));

            QM_TRY_INSPECT(const bool& exists,
                           MOZ_TO_RESULT_INVOKE_MEMBER(directory, Exists));

            QM_TRY(OkIf(exists), Err(NS_ERROR_FILE_NOT_FOUND));

            QM_TRY_INSPECT(const bool& isDirectory,
                           MOZ_TO_RESULT_INVOKE_MEMBER(directory, IsDirectory));

            QM_TRY(OkIf(isDirectory), Err(NS_ERROR_FILE_DESTINATION_NOT_DIR));

            // Calling LoadFullOriginMetadataWithRestore might update the group
            // in the metadata file, but only as a side-effect. The actual place
            // we ensure consistency is in EnsureTemporaryOriginIsInitialized.

            QM_TRY_INSPECT(const auto& metadata,
                           LoadFullOriginMetadataWithRestore(directory));

            QM_TRY(OkIf(fullOriginMetadata.mLastAccessTime ==
                        metadata.mLastAccessTime),
                   Err(NS_ERROR_FAILURE));

            QM_TRY(OkIf(fullOriginMetadata.mPersisted == metadata.mPersisted),
                   Err(NS_ERROR_FAILURE));

            QM_TRY(OkIf(fullOriginMetadata.mPersistenceType ==
                        metadata.mPersistenceType),
                   Err(NS_ERROR_FAILURE));

            QM_TRY(OkIf(fullOriginMetadata.mSuffix == metadata.mSuffix),
                   Err(NS_ERROR_FAILURE));

            QM_TRY(OkIf(fullOriginMetadata.mGroup == metadata.mGroup),
                   Err(NS_ERROR_FAILURE));

            QM_TRY(OkIf(fullOriginMetadata.mOrigin == metadata.mOrigin),
                   Err(NS_ERROR_FAILURE));

            QM_TRY(OkIf(fullOriginMetadata.mStorageOrigin ==
                        metadata.mStorageOrigin),
                   Err(NS_ERROR_FAILURE));

            QM_TRY(OkIf(fullOriginMetadata.mIsPrivate == metadata.mIsPrivate),
                   Err(NS_ERROR_FAILURE));

            QM_TRY(MOZ_TO_RESULT(InitializeOrigin(
                fullOriginMetadata.mPersistenceType, fullOriginMetadata,
                fullOriginMetadata.mLastAccessTime,
                fullOriginMetadata.mPersisted, directory)));
          } else {
            InitQuotaForOrigin(fullOriginMetadata, clientUsages, usage);
          }

          MaybeCollectUnaccessedOrigin(fullOriginMetadata);

          return Ok{};
        }));

    autoRemoveQuota.release();

    return NS_OK;
  };

  QM_TRY_INSPECT(
      const bool& loadQuotaFromCache, ([this]() -> Result<bool, nsresult> {
        if (mCacheUsable) {
          QM_TRY_INSPECT(
              const auto& stmt,
              CreateAndExecuteSingleStepStatement<
                  SingleStepResult::ReturnNullIfNoResult>(
                  *mStorageConnection, "SELECT valid, build_id FROM cache"_ns));

          QM_TRY(OkIf(stmt), Err(NS_ERROR_FILE_CORRUPTED));

          QM_TRY_INSPECT(const int32_t& valid,
                         MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt32, 0));

          if (valid) {
            if (!StaticPrefs::dom_quotaManager_caching_checkBuildId()) {
              return true;
            }

            QM_TRY_INSPECT(const auto& buildId,
                           MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
                               nsAutoCString, stmt, GetUTF8String, 1));

            return buildId == *gBuildId;
          }
        }

        return false;
      }()));

  auto autoRemoveQuota = MakeScopeExit([&] { RemoveQuota(); });

  if (!loadQuotaFromCache ||
      !StaticPrefs::dom_quotaManager_loadQuotaFromCache() ||
      ![&LoadQuotaFromCache] {
        QM_WARNONLY_TRY_UNWRAP(auto res, MOZ_TO_RESULT(LoadQuotaFromCache()));
        return static_cast<bool>(res);
      }()) {
    // A keeper to defer the return only in Nightly, so that the telemetry data
    // for whole profile can be collected.
#ifdef NIGHTLY_BUILD
    nsresult statusKeeper = NS_OK;
#endif

    const auto statusKeeperFunc = [&](const nsresult rv) {
      RECORD_IN_NIGHTLY(statusKeeper, rv);
    };

    for (const PersistenceType type :
         kInitializableBestEffortPersistenceTypes) {
      if (NS_WARN_IF(IsShuttingDown())) {
        RETURN_STATUS_OR_RESULT(statusKeeper, NS_ERROR_ABORT);
      }

      QM_TRY(([&]() -> Result<Ok, nsresult> {
        QM_TRY(MOZ_TO_RESULT(([this, type, &MaybeCollectUnaccessedOrigin] {
                 const auto innerFunc = [&](const auto&) -> nsresult {
                   return InitializeRepository(type,
                                               MaybeCollectUnaccessedOrigin);
                 };

                 return ExecuteInitialization(
                     type == PERSISTENCE_TYPE_DEFAULT
                         ? Initialization::DefaultRepository
                         : Initialization::TemporaryRepository,
                     innerFunc);
               }())),
               OK_IN_NIGHTLY_PROPAGATE_IN_OTHERS, statusKeeperFunc);

        return Ok{};
      }()));
    }

#ifdef NIGHTLY_BUILD
    if (NS_FAILED(statusKeeper)) {
      return statusKeeper;
    }
#endif
  }

  autoRemoveQuota.release();

  const auto endTime = recordQuotaInfoLoadTimeHelper->End();

  if (StaticPrefs::dom_quotaManager_checkQuotaInfoLoadTime() &&
      static_cast<uint32_t>((endTime - startTime).ToMilliseconds()) >=
          StaticPrefs::dom_quotaManager_longQuotaInfoLoadTimeThresholdMs() &&
      !unaccessedOrigins.IsEmpty()) {
    QM_WARNONLY_TRY(ArchiveOrigins(unaccessedOrigins));
  }

  return NS_OK;
}

void QuotaManager::UnloadQuota() {
  AssertIsOnIOThread();
  MOZ_ASSERT(mStorageConnection);
  MOZ_ASSERT(mTemporaryStorageInitialized);
  MOZ_ASSERT(mCacheUsable);

  auto autoRemoveQuota = MakeScopeExit([&] { RemoveQuota(); });

  mozStorageTransaction transaction(
      mStorageConnection, false, mozIStorageConnection::TRANSACTION_IMMEDIATE);

  QM_TRY(MOZ_TO_RESULT(transaction.Start()), QM_VOID);

  QM_TRY(MOZ_TO_RESULT(
             mStorageConnection->ExecuteSimpleSQL("DELETE FROM origin;"_ns)),
         QM_VOID);

  nsCOMPtr<mozIStorageStatement> insertStmt;

  {
    MutexAutoLock lock(mQuotaMutex);

    for (auto iter = mGroupInfoPairs.Iter(); !iter.Done(); iter.Next()) {
      MOZ_ASSERT(!iter.Key().IsEmpty());

      GroupInfoPair* const pair = iter.UserData();
      MOZ_ASSERT(pair);

      for (const PersistenceType type : kBestEffortPersistenceTypes) {
        RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(type);
        if (!groupInfo) {
          continue;
        }

        for (const auto& originInfo : groupInfo->mOriginInfos) {
          MOZ_ASSERT(!originInfo->mCanonicalQuotaObjects.Count());

          if (!originInfo->mDirectoryExists) {
            continue;
          }

          if (originInfo->mIsPrivate) {
            continue;
          }

          if (insertStmt) {
            MOZ_ALWAYS_SUCCEEDS(insertStmt->Reset());
          } else {
            QM_TRY_UNWRAP(
                insertStmt,
                MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
                    nsCOMPtr<mozIStorageStatement>, mStorageConnection,
                    CreateStatement,
                    "INSERT INTO origin (repository_id, suffix, group_, "
                    "origin, client_usages, usage, last_access_time, "
                    "accessed, persisted) "
                    "VALUES (:repository_id, :suffix, :group_, :origin, "
                    ":client_usages, :usage, :last_access_time, :accessed, "
                    ":persisted)"_ns),
                QM_VOID);
          }

          QM_TRY(MOZ_TO_RESULT(originInfo->LockedBindToStatement(insertStmt)),
                 QM_VOID);

          QM_TRY(MOZ_TO_RESULT(insertStmt->Execute()), QM_VOID);
        }

        groupInfo->LockedRemoveOriginInfos();
      }

      iter.Remove();
    }
  }

  QM_TRY_INSPECT(
      const auto& stmt,
      MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
          nsCOMPtr<mozIStorageStatement>, mStorageConnection, CreateStatement,
          "UPDATE cache SET valid = :valid, build_id = :buildId;"_ns),
      QM_VOID);

  QM_TRY(MOZ_TO_RESULT(stmt->BindInt32ByName("valid"_ns, 1)), QM_VOID);
  QM_TRY(MOZ_TO_RESULT(stmt->BindUTF8StringByName("buildId"_ns, *gBuildId)),
         QM_VOID);
  QM_TRY(MOZ_TO_RESULT(stmt->Execute()), QM_VOID);
  QM_TRY(MOZ_TO_RESULT(transaction.Commit()), QM_VOID);
}

already_AddRefed<QuotaObject> QuotaManager::GetQuotaObject(
    PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata,
    Client::Type aClientType, nsIFile* aFile, int64_t aFileSize,
    int64_t* aFileSizeOut /* = nullptr */) {
  NS_ASSERTION(!NS_IsMainThread(), "Wrong thread!");
  MOZ_ASSERT(aOriginMetadata.mPersistenceType == aPersistenceType);

  if (aFileSizeOut) {
    *aFileSizeOut = 0;
  }

  if (aPersistenceType == PERSISTENCE_TYPE_PERSISTENT) {
    return nullptr;
  }

  QM_TRY_INSPECT(const auto& path,
                 MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, aFile, GetPath),
                 nullptr);

#ifdef DEBUG
  {
    QM_TRY_INSPECT(const auto& directory, GetOriginDirectory(aOriginMetadata),
                   nullptr);

    nsAutoString clientType;
    QM_TRY(OkIf(Client::TypeToText(aClientType, clientType, fallible)),
           nullptr);

    QM_TRY(MOZ_TO_RESULT(directory->Append(clientType)), nullptr);

    QM_TRY_INSPECT(
        const auto& directoryPath,
        MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, directory, GetPath),
        nullptr);

    MOZ_ASSERT(StringBeginsWith(path, directoryPath));
  }
#endif

  QM_TRY_INSPECT(
      const int64_t fileSize,
      ([&aFile, aFileSize]() -> Result<int64_t, nsresult> {
        if (aFileSize == -1) {
          QM_TRY_INSPECT(const bool& exists,
                         MOZ_TO_RESULT_INVOKE_MEMBER(aFile, Exists));

          if (exists) {
            QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(aFile, GetFileSize));
          }

          return 0;
        }

        return aFileSize;
      }()),
      nullptr);

  RefPtr<QuotaObject> result;
  {
    MutexAutoLock lock(mQuotaMutex);

    GroupInfoPair* pair;
    if (!mGroupInfoPairs.Get(aOriginMetadata.mGroup, &pair)) {
      return nullptr;
    }

    RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(aPersistenceType);

    if (!groupInfo) {
      return nullptr;
    }

    RefPtr<OriginInfo> originInfo =
        groupInfo->LockedGetOriginInfo(aOriginMetadata.mOrigin);

    if (!originInfo) {
      return nullptr;
    }

    // We need this extra raw pointer because we can't assign to the smart
    // pointer directly since QuotaObject::AddRef would try to acquire the same
    // mutex.
    const NotNull<CanonicalQuotaObject*> canonicalQuotaObject =
        originInfo->mCanonicalQuotaObjects.LookupOrInsertWith(path, [&] {
          // Create a new QuotaObject. The hashtable is not responsible to
          // delete the QuotaObject.
          return WrapNotNullUnchecked(new CanonicalQuotaObject(
              originInfo, aClientType, path, fileSize));
        });

    // Addref the QuotaObject and move the ownership to the result. This must
    // happen before we unlock!
    result = canonicalQuotaObject->LockedAddRef();
  }

  if (aFileSizeOut) {
    *aFileSizeOut = fileSize;
  }

  // The caller becomes the owner of the QuotaObject, that is, the caller is
  // is responsible to delete it when the last reference is removed.
  return result.forget();
}

already_AddRefed<QuotaObject> QuotaManager::GetQuotaObject(
    PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata,
    Client::Type aClientType, const nsAString& aPath, int64_t aFileSize,
    int64_t* aFileSizeOut /* = nullptr */) {
  NS_ASSERTION(!NS_IsMainThread(), "Wrong thread!");

  if (aFileSizeOut) {
    *aFileSizeOut = 0;
  }

  QM_TRY_INSPECT(const auto& file, QM_NewLocalFile(aPath), nullptr);

  return GetQuotaObject(aPersistenceType, aOriginMetadata, aClientType, file,
                        aFileSize, aFileSizeOut);
}

already_AddRefed<QuotaObject> QuotaManager::GetQuotaObject(
    const int64_t aDirectoryLockId, const nsAString& aPath) {
  NS_ASSERTION(!NS_IsMainThread(), "Wrong thread!");

  Maybe<MutexAutoLock> lock;

  // See the comment for mDirectoryLockIdTable in QuotaManager.h
  if (!IsOnBackgroundThread()) {
    lock.emplace(mQuotaMutex);
  }

  if (auto maybeDirectoryLock =
          mDirectoryLockIdTable.MaybeGet(aDirectoryLockId)) {
    const auto& directoryLock = *maybeDirectoryLock;
    MOZ_DIAGNOSTIC_ASSERT(directoryLock->ShouldUpdateLockIdTable());

    const PersistenceType persistenceType = directoryLock->GetPersistenceType();
    const OriginMetadata& originMetadata = directoryLock->OriginMetadata();
    const Client::Type clientType = directoryLock->ClientType();

    lock.reset();

    return GetQuotaObject(persistenceType, originMetadata, clientType, aPath);
  }

  MOZ_CRASH("Getting quota object for an unregistered directory lock?");
}

Nullable<bool> QuotaManager::OriginPersisted(
    const OriginMetadata& aOriginMetadata) {
  AssertIsOnIOThread();

  MutexAutoLock lock(mQuotaMutex);

  RefPtr<OriginInfo> originInfo =
      LockedGetOriginInfo(PERSISTENCE_TYPE_DEFAULT, aOriginMetadata);
  if (originInfo) {
    return Nullable<bool>(originInfo->LockedPersisted());
  }

  return Nullable<bool>();
}

void QuotaManager::PersistOrigin(const OriginMetadata& aOriginMetadata) {
  AssertIsOnIOThread();

  MutexAutoLock lock(mQuotaMutex);

  RefPtr<OriginInfo> originInfo =
      LockedGetOriginInfo(PERSISTENCE_TYPE_DEFAULT, aOriginMetadata);
  if (originInfo && !originInfo->LockedPersisted()) {
    originInfo->LockedPersist();
  }
}

void QuotaManager::AbortOperationsForLocks(
    const DirectoryLockIdTableArray& aLockIds) {
  for (Client::Type type : AllClientTypes()) {
    if (aLockIds[type].Filled()) {
      (*mClients)[type]->AbortOperationsForLocks(aLockIds[type]);
    }
  }
}

void QuotaManager::AbortOperationsForProcess(ContentParentId aContentParentId) {
  AssertIsOnOwningThread();

  for (const RefPtr<Client>& client : *mClients) {
    client->AbortOperationsForProcess(aContentParentId);
  }
}

Result<nsCOMPtr<nsIFile>, nsresult> QuotaManager::GetOriginDirectory(
    const OriginMetadata& aOriginMetadata) const {
  QM_TRY_UNWRAP(
      auto directory,
      QM_NewLocalFile(GetStoragePath(aOriginMetadata.mPersistenceType)));

  QM_TRY(MOZ_TO_RESULT(directory->Append(
      MakeSanitizedOriginString(aOriginMetadata.mStorageOrigin))));

  return directory;
}

// static
nsresult QuotaManager::CreateDirectoryMetadata(
    nsIFile& aDirectory, int64_t aTimestamp,
    const OriginMetadata& aOriginMetadata) {
  AssertIsOnIOThread();

  OriginAttributes groupAttributes;

  nsCString groupNoSuffix;
  QM_TRY(OkIf(groupAttributes.PopulateFromOrigin(aOriginMetadata.mGroup,
                                                 groupNoSuffix)),
         NS_ERROR_FAILURE);

  nsCString groupPrefix;
  GetJarPrefix(groupAttributes.mInIsolatedMozBrowser, groupPrefix);

  nsCString group = groupPrefix + groupNoSuffix;

  OriginAttributes originAttributes;

  nsCString originNoSuffix;
  QM_TRY(OkIf(originAttributes.PopulateFromOrigin(aOriginMetadata.mOrigin,
                                                  originNoSuffix)),
         NS_ERROR_FAILURE);

  nsCString originPrefix;
  GetJarPrefix(originAttributes.mInIsolatedMozBrowser, originPrefix);

  nsCString origin = originPrefix + originNoSuffix;

  MOZ_ASSERT(groupPrefix == originPrefix);

  QM_TRY_INSPECT(const auto& file, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
                                       nsCOMPtr<nsIFile>, aDirectory, Clone));

  QM_TRY(MOZ_TO_RESULT(file->Append(nsLiteralString(METADATA_TMP_FILE_NAME))));

  QM_TRY_INSPECT(const auto& stream,
                 GetBinaryOutputStream(*file, FileFlag::Truncate));
  MOZ_ASSERT(stream);

  QM_TRY(MOZ_TO_RESULT(stream->Write64(aTimestamp)));

  QM_TRY(MOZ_TO_RESULT(stream->WriteStringZ(group.get())));

  QM_TRY(MOZ_TO_RESULT(stream->WriteStringZ(origin.get())));

  // Currently unused (used to be isApp).
  QM_TRY(MOZ_TO_RESULT(stream->WriteBoolean(false)));

  QM_TRY(MOZ_TO_RESULT(stream->Flush()));

  QM_TRY(MOZ_TO_RESULT(stream->Close()));

  QM_TRY(MOZ_TO_RESULT(
      file->RenameTo(nullptr, nsLiteralString(METADATA_FILE_NAME))));

  return NS_OK;
}

// static
nsresult QuotaManager::CreateDirectoryMetadata2(
    nsIFile& aDirectory, int64_t aTimestamp, bool aPersisted,
    const OriginMetadata& aOriginMetadata) {
  AssertIsOnIOThread();

  QM_TRY_INSPECT(const auto& file, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
                                       nsCOMPtr<nsIFile>, aDirectory, Clone));

  QM_TRY(
      MOZ_TO_RESULT(file->Append(nsLiteralString(METADATA_V2_TMP_FILE_NAME))));

  QM_TRY_INSPECT(const auto& stream,
                 GetBinaryOutputStream(*file, FileFlag::Truncate));
  MOZ_ASSERT(stream);

  QM_TRY(MOZ_TO_RESULT(stream->Write64(aTimestamp)));

  QM_TRY(MOZ_TO_RESULT(stream->WriteBoolean(aPersisted)));

  // Reserved data 1
  QM_TRY(MOZ_TO_RESULT(stream->Write32(0)));

  // Reserved data 2
  QM_TRY(MOZ_TO_RESULT(stream->Write32(0)));

  // Currently unused (used to be suffix).
  QM_TRY(MOZ_TO_RESULT(stream->WriteStringZ("")));

  // Currently unused (used to be group).
  QM_TRY(MOZ_TO_RESULT(stream->WriteStringZ("")));

  QM_TRY(MOZ_TO_RESULT(
      stream->WriteStringZ(aOriginMetadata.mStorageOrigin.get())));

  // Currently used for isPrivate (used to be used for isApp).
  QM_TRY(MOZ_TO_RESULT(stream->WriteBoolean(aOriginMetadata.mIsPrivate)));

  QM_TRY(MOZ_TO_RESULT(stream->Flush()));

  QM_TRY(MOZ_TO_RESULT(stream->Close()));

  QM_TRY(MOZ_TO_RESULT(
      file->RenameTo(nullptr, nsLiteralString(METADATA_V2_FILE_NAME))));

  return NS_OK;
}

nsresult QuotaManager::RestoreDirectoryMetadata2(nsIFile* aDirectory) {
  AssertIsOnIOThread();
  MOZ_ASSERT(aDirectory);
  MOZ_ASSERT(mStorageConnection);

  RefPtr<RestoreDirectoryMetadata2Helper> helper =
      new RestoreDirectoryMetadata2Helper(aDirectory);

  QM_TRY(MOZ_TO_RESULT(helper->Init()));

  QM_TRY(MOZ_TO_RESULT(helper->RestoreMetadata2File()));

  return NS_OK;
}

Result<FullOriginMetadata, nsresult> QuotaManager::LoadFullOriginMetadata(
    nsIFile* aDirectory, PersistenceType aPersistenceType) {
  MOZ_ASSERT(!NS_IsMainThread());
  MOZ_ASSERT(aDirectory);
  MOZ_ASSERT(mStorageConnection);

  QM_TRY_INSPECT(const auto& binaryStream,
                 GetBinaryInputStream(*aDirectory,
                                      nsLiteralString(METADATA_V2_FILE_NAME)));

  FullOriginMetadata fullOriginMetadata;

  QM_TRY_UNWRAP(fullOriginMetadata.mLastAccessTime,
                MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, Read64));

  QM_TRY_UNWRAP(fullOriginMetadata.mPersisted,
                MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, ReadBoolean));

  QM_TRY_INSPECT(const bool& reservedData1,
                 MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, Read32));
  Unused << reservedData1;

  // XXX Use for the persistence type.
  QM_TRY_INSPECT(const bool& reservedData2,
                 MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, Read32));
  Unused << reservedData2;

  fullOriginMetadata.mPersistenceType = aPersistenceType;

  QM_TRY_INSPECT(const auto& suffix, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
                                         nsCString, binaryStream, ReadCString));
  Unused << suffix;

  QM_TRY_INSPECT(const auto& group, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
                                        nsCString, binaryStream, ReadCString));
  Unused << group;

  QM_TRY_UNWRAP(
      fullOriginMetadata.mStorageOrigin,
      MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, binaryStream, ReadCString));

  // Currently used for isPrivate (used to be used for isApp).
  QM_TRY_UNWRAP(fullOriginMetadata.mIsPrivate,
                MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, ReadBoolean));

  QM_TRY(MOZ_TO_RESULT(binaryStream->Close()));

  auto principal =
      [&storageOrigin =
           fullOriginMetadata.mStorageOrigin]() -> nsCOMPtr<nsIPrincipal> {
    if (storageOrigin.EqualsLiteral(kChromeOrigin)) {
      return SystemPrincipal::Get();
    }
    return BasePrincipal::CreateContentPrincipal(storageOrigin);
  }();
  QM_TRY(MOZ_TO_RESULT(principal));

  PrincipalInfo principalInfo;
  QM_TRY(MOZ_TO_RESULT(PrincipalToPrincipalInfo(principal, &principalInfo)));

  QM_TRY(MOZ_TO_RESULT(IsPrincipalInfoValid(principalInfo)),
         Err(NS_ERROR_MALFORMED_URI));

  QM_TRY_UNWRAP(auto principalMetadata,
                GetInfoFromValidatedPrincipalInfo(principalInfo));

  fullOriginMetadata.mSuffix = std::move(principalMetadata.mSuffix);
  fullOriginMetadata.mGroup = std::move(principalMetadata.mGroup);
  fullOriginMetadata.mOrigin = std::move(principalMetadata.mOrigin);

  QM_TRY_INSPECT(const bool& groupUpdated,
                 MaybeUpdateGroupForOrigin(fullOriginMetadata));

  // A workaround for a bug in GetLastModifiedTime implementation which should
  // have returned the current time instead of INT64_MIN when there were no
  // suitable files for getting last modified time.
  QM_TRY_INSPECT(const bool& lastAccessTimeUpdated,
                 MaybeUpdateLastAccessTimeForOrigin(fullOriginMetadata));

  if (groupUpdated || lastAccessTimeUpdated) {
    // Only overwriting .metadata-v2 (used to overwrite .metadata too) to reduce
    // I/O.
    QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata2(
        *aDirectory, fullOriginMetadata.mLastAccessTime,
        fullOriginMetadata.mPersisted, fullOriginMetadata)));
  }

  return fullOriginMetadata;
}

Result<FullOriginMetadata, nsresult>
QuotaManager::LoadFullOriginMetadataWithRestore(nsIFile* aDirectory) {
  // XXX Once the persistence type is stored in the metadata file, this block
  // for getting the persistence type from the parent directory name can be
  // removed.
  nsCOMPtr<nsIFile> parentDir;
  QM_TRY(MOZ_TO_RESULT(aDirectory->GetParent(getter_AddRefs(parentDir))));

  const auto maybePersistenceType =
      PersistenceTypeFromFile(*parentDir, fallible);
  QM_TRY(OkIf(maybePersistenceType.isSome()), Err(NS_ERROR_FAILURE));

  const auto& persistenceType = maybePersistenceType.value();

  QM_TRY_RETURN(QM_OR_ELSE_WARN(
      // Expression.
      LoadFullOriginMetadata(aDirectory, persistenceType),
      // Fallback.
      ([&aDirectory, &persistenceType,
        this](const nsresult rv) -> Result<FullOriginMetadata, nsresult> {
        QM_TRY(MOZ_TO_RESULT(RestoreDirectoryMetadata2(aDirectory)));

        QM_TRY_RETURN(LoadFullOriginMetadata(aDirectory, persistenceType));
      })));
}

Result<OriginMetadata, nsresult> QuotaManager::GetOriginMetadata(
    nsIFile* aDirectory) {
  MOZ_ASSERT(aDirectory);
  MOZ_ASSERT(mStorageConnection);

  QM_TRY_INSPECT(
      const auto& leafName,
      MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoString, aDirectory, GetLeafName));

  nsCString spec;
  OriginAttributes attrs;
  nsCString originalSuffix;
  OriginParser::ResultType result = OriginParser::ParseOrigin(
      NS_ConvertUTF16toUTF8(leafName), spec, &attrs, originalSuffix);
  QM_TRY(MOZ_TO_RESULT(result == OriginParser::ValidOrigin));

  QM_TRY_INSPECT(
      const auto& principal,
      ([&spec, &attrs]() -> Result<nsCOMPtr<nsIPrincipal>, nsresult> {
        if (spec.EqualsLiteral(kChromeOrigin)) {
          return nsCOMPtr<nsIPrincipal>(SystemPrincipal::Get());
        }

        nsCOMPtr<nsIURI> uri;
        QM_TRY(MOZ_TO_RESULT(NS_NewURI(getter_AddRefs(uri), spec)));

        return nsCOMPtr<nsIPrincipal>(
            BasePrincipal::CreateContentPrincipal(uri, attrs));
      }()));
  QM_TRY(MOZ_TO_RESULT(principal));

  PrincipalInfo principalInfo;
  QM_TRY(MOZ_TO_RESULT(PrincipalToPrincipalInfo(principal, &principalInfo)));

  QM_TRY(MOZ_TO_RESULT(IsPrincipalInfoValid(principalInfo)),
         Err(NS_ERROR_MALFORMED_URI));

  QM_TRY_UNWRAP(auto principalMetadata,
                GetInfoFromValidatedPrincipalInfo(principalInfo));

  QM_TRY_INSPECT(const auto& parentDirectory,
                 MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCOMPtr<nsIFile>,
                                                   aDirectory, GetParent));

  const auto maybePersistenceType =
      PersistenceTypeFromFile(*parentDirectory, fallible);
  QM_TRY(MOZ_TO_RESULT(maybePersistenceType.isSome()));

  return OriginMetadata{std::move(principalMetadata),
                        maybePersistenceType.value()};
}

template <typename OriginFunc>
nsresult QuotaManager::InitializeRepository(PersistenceType aPersistenceType,
                                            OriginFunc&& aOriginFunc) {
  AssertIsOnIOThread();
  MOZ_ASSERT(aPersistenceType == PERSISTENCE_TYPE_TEMPORARY ||
             aPersistenceType == PERSISTENCE_TYPE_DEFAULT);

  QM_TRY_INSPECT(const auto& directory,
                 QM_NewLocalFile(GetStoragePath(aPersistenceType)));

  QM_TRY_INSPECT(const bool& created, EnsureDirectory(*directory));

  Unused << created;

  // A keeper to defer the return only in Nightly, so that the telemetry data
  // for whole profile can be collected
#ifdef NIGHTLY_BUILD
  nsresult statusKeeper = NS_OK;
#endif

  const auto statusKeeperFunc = [&](const nsresult rv) {
    RECORD_IN_NIGHTLY(statusKeeper, rv);
  };

  struct RenameAndInitInfo {
    nsCOMPtr<nsIFile> mOriginDirectory;
    FullOriginMetadata mFullOriginMetadata;
    int64_t mTimestamp;
    bool mPersisted;
  };
  nsTArray<RenameAndInitInfo> renameAndInitInfos;

  QM_TRY(([&]() -> Result<Ok, nsresult> {
    QM_TRY(
        CollectEachFile(
            *directory,
            [&](nsCOMPtr<nsIFile>&& childDirectory) -> Result<Ok, nsresult> {
              if (NS_WARN_IF(IsShuttingDown())) {
                RETURN_STATUS_OR_RESULT(statusKeeper, NS_ERROR_ABORT);
              }

              QM_TRY(
                  ([this, &childDirectory, &renameAndInitInfos,
                    aPersistenceType, &aOriginFunc]() -> Result<Ok, nsresult> {
                    QM_TRY_INSPECT(
                        const auto& leafName,
                        MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
                            nsAutoString, childDirectory, GetLeafName));

                    QM_TRY_INSPECT(const auto& dirEntryKind,
                                   GetDirEntryKind(*childDirectory));

                    switch (dirEntryKind) {
                      case nsIFileKind::ExistsAsDirectory: {
                        QM_TRY_UNWRAP(
                            auto maybeMetadata,
                            QM_OR_ELSE_WARN_IF(
                                // Expression
                                LoadFullOriginMetadataWithRestore(
                                    childDirectory)
                                    .map([](auto metadata)
                                             -> Maybe<FullOriginMetadata> {
                                      return Some(std::move(metadata));
                                    }),
                                // Predicate.
                                IsSpecificError<NS_ERROR_MALFORMED_URI>,
                                // Fallback.
                                ErrToDefaultOk<Maybe<FullOriginMetadata>>));

                        if (!maybeMetadata) {
                          // Unknown directories during initialization are
                          // allowed. Just warn if we find them.
                          UNKNOWN_FILE_WARNING(leafName);
                          break;
                        }

                        auto metadata = maybeMetadata.extract();

                        MOZ_ASSERT(metadata.mPersistenceType ==
                                   aPersistenceType);

                        // FIXME(tt): The check for origin name consistency can
                        // be removed once we have an upgrade to traverse origin
                        // directories and check through the directory metadata
                        // files.
                        const auto originSanitized =
                            MakeSanitizedOriginCString(metadata.mOrigin);

                        NS_ConvertUTF16toUTF8 utf8LeafName(leafName);
                        if (!originSanitized.Equals(utf8LeafName)) {
                          QM_WARNING(
                              "The name of the origin directory (%s) doesn't "
                              "match the sanitized origin string (%s) in the "
                              "metadata file!",
                              utf8LeafName.get(), originSanitized.get());

                          // If it's the known case, we try to restore the
                          // origin directory name if it's possible.
                          if (originSanitized.Equals(utf8LeafName + "."_ns)) {
                            const int64_t lastAccessTime =
                                metadata.mLastAccessTime;
                            const bool persisted = metadata.mPersisted;
                            renameAndInitInfos.AppendElement(RenameAndInitInfo{
                                std::move(childDirectory), std::move(metadata),
                                lastAccessTime, persisted});
                            break;
                          }

                          // XXXtt: Try to restore the unknown cases base on the
                          // content for their metadata files. Note that if the
                          // restore fails, QM should maintain a list and ensure
                          // they won't be accessed after initialization.
                        }

                        QM_TRY(QM_OR_ELSE_WARN_IF(
                            // Expression.
                            MOZ_TO_RESULT(InitializeOrigin(
                                aPersistenceType, metadata,
                                metadata.mLastAccessTime, metadata.mPersisted,
                                childDirectory)),
                            // Predicate.
                            IsDatabaseCorruptionError,
                            // Fallback.
                            ([&childDirectory](
                                 const nsresult rv) -> Result<Ok, nsresult> {
                              // If the origin can't be initialized due to
                              // corruption, this is a permanent
                              // condition, and we need to remove all data
                              // for the origin on disk.

                              QM_TRY(
                                  MOZ_TO_RESULT(childDirectory->Remove(true)));

                              return Ok{};
                            })));

                        std::forward<OriginFunc>(aOriginFunc)(metadata);

                        break;
                      }

                      case nsIFileKind::ExistsAsFile:
                        if (IsOSMetadata(leafName) || IsDotFile(leafName)) {
                          break;
                        }

                        // Unknown files during initialization are now allowed.
                        // Just warn if we find them.
                        UNKNOWN_FILE_WARNING(leafName);
                        break;

                      case nsIFileKind::DoesNotExist:
                        // Ignore files that got removed externally while
                        // iterating.
                        break;
                    }

                    return Ok{};
                  }()),
                  OK_IN_NIGHTLY_PROPAGATE_IN_OTHERS, statusKeeperFunc);

              return Ok{};
            }),
        OK_IN_NIGHTLY_PROPAGATE_IN_OTHERS, statusKeeperFunc);

    return Ok{};
  }()));

  for (auto& info : renameAndInitInfos) {
    QM_TRY(([&]() -> Result<Ok, nsresult> {
      QM_TRY(([&directory, &info, this, aPersistenceType,
               &aOriginFunc]() -> Result<Ok, nsresult> {
               const auto originDirName =
                   MakeSanitizedOriginString(info.mFullOriginMetadata.mOrigin);

               // Check if targetDirectory exist.
               QM_TRY_INSPECT(const auto& targetDirectory,
                              CloneFileAndAppend(*directory, originDirName));

               QM_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE_MEMBER(
                                                      targetDirectory, Exists));

               if (exists) {
                 QM_TRY(MOZ_TO_RESULT(info.mOriginDirectory->Remove(true)));

                 return Ok{};
               }

               QM_TRY(MOZ_TO_RESULT(
                   info.mOriginDirectory->RenameTo(nullptr, originDirName)));

               // XXX We don't check corruption here ?
               QM_TRY(MOZ_TO_RESULT(InitializeOrigin(
                   aPersistenceType, info.mFullOriginMetadata, info.mTimestamp,
                   info.mPersisted, targetDirectory)));

               std::forward<OriginFunc>(aOriginFunc)(info.mFullOriginMetadata);

               return Ok{};
             }()),
             OK_IN_NIGHTLY_PROPAGATE_IN_OTHERS, statusKeeperFunc);

      return Ok{};
    }()));
  }

#ifdef NIGHTLY_BUILD
  if (NS_FAILED(statusKeeper)) {
    return statusKeeper;
  }
#endif

  return NS_OK;
}

nsresult QuotaManager::InitializeOrigin(PersistenceType aPersistenceType,
                                        const OriginMetadata& aOriginMetadata,
                                        int64_t aAccessTime, bool aPersisted,
                                        nsIFile* aDirectory) {
  AssertIsOnIOThread();

  const bool trackQuota = aPersistenceType != PERSISTENCE_TYPE_PERSISTENT;

  // We need to initialize directories of all clients if they exists and also
  // get the total usage to initialize the quota.

  ClientUsageArray clientUsages;

  // A keeper to defer the return only in Nightly, so that the telemetry data
  // for whole profile can be collected
#ifdef NIGHTLY_BUILD
  nsresult statusKeeper = NS_OK;
#endif

  QM_TRY(([&, statusKeeperFunc = [&](const nsresult rv) {
            RECORD_IN_NIGHTLY(statusKeeper, rv);
          }]() -> Result<Ok, nsresult> {
    QM_TRY(
        CollectEachFile(
            *aDirectory,
            [&](const nsCOMPtr<nsIFile>& file) -> Result<Ok, nsresult> {
              if (NS_WARN_IF(IsShuttingDown())) {
                RETURN_STATUS_OR_RESULT(statusKeeper, NS_ERROR_ABORT);
              }

              QM_TRY(
                  ([this, &file, trackQuota, aPersistenceType, &aOriginMetadata,
                    &clientUsages]() -> Result<Ok, nsresult> {
                    QM_TRY_INSPECT(const auto& leafName,
                                   MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
                                       nsAutoString, file, GetLeafName));

                    QM_TRY_INSPECT(const auto& dirEntryKind,
                                   GetDirEntryKind(*file));

                    switch (dirEntryKind) {
                      case nsIFileKind::ExistsAsDirectory: {
                        Client::Type clientType;
                        const bool ok = Client::TypeFromText(
                            leafName, clientType, fallible);
                        if (!ok) {
                          // Unknown directories during initialization are now
                          // allowed. Just warn if we find them.
                          UNKNOWN_FILE_WARNING(leafName);
                          break;
                        }

                        if (trackQuota) {
                          QM_TRY_INSPECT(
                              const auto& usageInfo,
                              (*mClients)[clientType]->InitOrigin(
                                  aPersistenceType, aOriginMetadata,
                                  /* aCanceled */ Atomic<bool>(false)));

                          MOZ_ASSERT(!clientUsages[clientType]);

                          if (usageInfo.TotalUsage()) {
                            // XXX(Bug 1683863) Until we identify the root cause
                            // of seemingly converted-from-negative usage
                            // values, we will just treat them as unset here,
                            // but log a warning to the browser console.
                            if (static_cast<int64_t>(*usageInfo.TotalUsage()) >=
                                0) {
                              clientUsages[clientType] = usageInfo.TotalUsage();
                            } else {
#if defined(EARLY_BETA_OR_EARLIER) || defined(DEBUG)
                              const nsCOMPtr<nsIConsoleService> console =
                                  do_GetService(NS_CONSOLESERVICE_CONTRACTID);
                              if (console) {
                                console->LogStringMessage(
                                    nsString(
                                        u"QuotaManager warning: client "_ns +
                                        leafName +
                                        u" reported negative usage for group "_ns +
                                        NS_ConvertUTF8toUTF16(
                                            aOriginMetadata.mGroup) +
                                        u", origin "_ns +
                                        NS_ConvertUTF8toUTF16(
                                            aOriginMetadata.mOrigin))
                                        .get());
                              }
#endif
                            }
                          }
                        } else {
                          QM_TRY(MOZ_TO_RESULT(
                              (*mClients)[clientType]
                                  ->InitOriginWithoutTracking(
                                      aPersistenceType, aOriginMetadata,
                                      /* aCanceled */ Atomic<bool>(false))));
                        }

                        break;
                      }

                      case nsIFileKind::ExistsAsFile:
                        if (IsOriginMetadata(leafName)) {
                          break;
                        }

                        if (IsTempMetadata(leafName)) {
                          QM_TRY(MOZ_TO_RESULT(
                              file->Remove(/* recursive */ false)));

                          break;
                        }

                        if (IsOSMetadata(leafName) || IsDotFile(leafName)) {
                          break;
                        }

                        // Unknown files during initialization are now allowed.
                        // Just warn if we find them.
                        UNKNOWN_FILE_WARNING(leafName);
                        // Bug 1595448 will handle the case for unknown files
                        // like idb, cache, or ls.
                        break;

                      case nsIFileKind::DoesNotExist:
                        // Ignore files that got removed externally while
                        // iterating.
                        break;
                    }

                    return Ok{};
                  }()),
                  OK_IN_NIGHTLY_PROPAGATE_IN_OTHERS, statusKeeperFunc);

              return Ok{};
            }),
        OK_IN_NIGHTLY_PROPAGATE_IN_OTHERS, statusKeeperFunc);

    return Ok{};
  }()));

#ifdef NIGHTLY_BUILD
  if (NS_FAILED(statusKeeper)) {
    return statusKeeper;
  }
#endif

  if (trackQuota) {
    const auto usage = std::accumulate(
        clientUsages.cbegin(), clientUsages.cend(), CheckedUint64(0),
        [](CheckedUint64 value, const Maybe<uint64_t>& clientUsage) {
          return value + clientUsage.valueOr(0);
        });

    // XXX Should we log more information, i.e. the whole clientUsages array, in
    // case usage is not valid?

    QM_TRY(OkIf(usage.isValid()), NS_ERROR_FAILURE);

    InitQuotaForOrigin(
        FullOriginMetadata{aOriginMetadata, aPersisted, aAccessTime},
        clientUsages, usage.value());
  }

  return NS_OK;
}

nsresult
QuotaManager::UpgradeFromIndexedDBDirectoryToPersistentStorageDirectory(
    nsIFile* aIndexedDBDir) {
  AssertIsOnIOThread();
  MOZ_ASSERT(aIndexedDBDir);

  const auto innerFunc = [this, &aIndexedDBDir](const auto&) -> nsresult {
    bool isDirectory;
    QM_TRY(MOZ_TO_RESULT(aIndexedDBDir->IsDirectory(&isDirectory)));

    if (!isDirectory) {
      NS_WARNING("indexedDB entry is not a directory!");
      return NS_OK;
    }

    auto persistentStorageDirOrErr = QM_NewLocalFile(*mStoragePath);
    if (NS_WARN_IF(persistentStorageDirOrErr.isErr())) {
      return persistentStorageDirOrErr.unwrapErr();
    }

    nsCOMPtr<nsIFile> persistentStorageDir = persistentStorageDirOrErr.unwrap();

    QM_TRY(MOZ_TO_RESULT(persistentStorageDir->Append(
        nsLiteralString(PERSISTENT_DIRECTORY_NAME))));

    bool exists;
    QM_TRY(MOZ_TO_RESULT(persistentStorageDir->Exists(&exists)));

    if (exists) {
      QM_WARNING("Deleting old <profile>/indexedDB directory!");

      QM_TRY(MOZ_TO_RESULT(aIndexedDBDir->Remove(/* aRecursive */ true)));

      return NS_OK;
    }

    nsCOMPtr<nsIFile> storageDir;
    QM_TRY(MOZ_TO_RESULT(
        persistentStorageDir->GetParent(getter_AddRefs(storageDir))));

    // MoveTo() is atomic if the move happens on the same volume which should
    // be our case, so even if we crash in the middle of the operation nothing
    // breaks next time we try to initialize.
    // However there's a theoretical possibility that the indexedDB directory
    // is on different volume, but it should be rare enough that we don't have
    // to worry about it.
    QM_TRY(MOZ_TO_RESULT(aIndexedDBDir->MoveTo(
        storageDir, nsLiteralString(PERSISTENT_DIRECTORY_NAME))));

    return NS_OK;
  };

  return ExecuteInitialization(Initialization::UpgradeFromIndexedDBDirectory,
                               innerFunc);
}

nsresult
QuotaManager::UpgradeFromPersistentStorageDirectoryToDefaultStorageDirectory(
    nsIFile* aPersistentStorageDir) {
  AssertIsOnIOThread();
  MOZ_ASSERT(aPersistentStorageDir);

  const auto innerFunc = [this,
                          &aPersistentStorageDir](const auto&) -> nsresult {
    QM_TRY_INSPECT(
        const bool& isDirectory,
        MOZ_TO_RESULT_INVOKE_MEMBER(aPersistentStorageDir, IsDirectory));

    if (!isDirectory) {
      NS_WARNING("persistent entry is not a directory!");
      return NS_OK;
    }

    {
      QM_TRY_INSPECT(const auto& defaultStorageDir,
                     QM_NewLocalFile(*mDefaultStoragePath));

      QM_TRY_INSPECT(const bool& exists,
                     MOZ_TO_RESULT_INVOKE_MEMBER(defaultStorageDir, Exists));

      if (exists) {
        QM_WARNING("Deleting old <profile>/storage/persistent directory!");

        QM_TRY(MOZ_TO_RESULT(
            aPersistentStorageDir->Remove(/* aRecursive */ true)));

        return NS_OK;
      }
    }

    {
      // Create real metadata files for origin directories in persistent
      // storage.
      auto helper = MakeRefPtr<CreateOrUpgradeDirectoryMetadataHelper>(
          aPersistentStorageDir);

      QM_TRY(MOZ_TO_RESULT(helper->Init()));

      QM_TRY(MOZ_TO_RESULT(helper->ProcessRepository()));

      // Upgrade metadata files for origin directories in temporary storage.
      QM_TRY_INSPECT(const auto& temporaryStorageDir,
                     QM_NewLocalFile(*mTemporaryStoragePath));

      QM_TRY_INSPECT(const bool& exists,
                     MOZ_TO_RESULT_INVOKE_MEMBER(temporaryStorageDir, Exists));

      if (exists) {
        QM_TRY_INSPECT(
            const bool& isDirectory,
            MOZ_TO_RESULT_INVOKE_MEMBER(temporaryStorageDir, IsDirectory));

        if (!isDirectory) {
          NS_WARNING("temporary entry is not a directory!");
          return NS_OK;
        }

        helper = MakeRefPtr<CreateOrUpgradeDirectoryMetadataHelper>(
            temporaryStorageDir);

        QM_TRY(MOZ_TO_RESULT(helper->Init()));

        QM_TRY(MOZ_TO_RESULT(helper->ProcessRepository()));
      }
    }

    // And finally rename persistent to default.
    QM_TRY(MOZ_TO_RESULT(aPersistentStorageDir->RenameTo(
        nullptr, nsLiteralString(DEFAULT_DIRECTORY_NAME))));

    return NS_OK;
  };

  return ExecuteInitialization(
      Initialization::UpgradeFromPersistentStorageDirectory, innerFunc);
}

template <typename Helper>
nsresult QuotaManager::UpgradeStorage(const int32_t aOldVersion,
                                      const int32_t aNewVersion,
                                      mozIStorageConnection* aConnection) {
  AssertIsOnIOThread();
  MOZ_ASSERT(aNewVersion > aOldVersion);
  MOZ_ASSERT(aNewVersion <= kStorageVersion);
  MOZ_ASSERT(aConnection);

  for (const PersistenceType persistenceType : kAllPersistenceTypes) {
    QM_TRY_UNWRAP(auto directory,
                  QM_NewLocalFile(GetStoragePath(persistenceType)));

    QM_TRY_INSPECT(const bool& exists,
                   MOZ_TO_RESULT_INVOKE_MEMBER(directory, Exists));

    if (!exists) {
      continue;
    }

    RefPtr<UpgradeStorageHelperBase> helper = new Helper(directory);

    QM_TRY(MOZ_TO_RESULT(helper->Init()));

    QM_TRY(MOZ_TO_RESULT(helper->ProcessRepository()));
  }

#ifdef DEBUG
  {
    QM_TRY_INSPECT(const int32_t& storageVersion,
                   MOZ_TO_RESULT_INVOKE_MEMBER(aConnection, GetSchemaVersion));

    MOZ_ASSERT(storageVersion == aOldVersion);
  }
#endif

  QM_TRY(MOZ_TO_RESULT(aConnection->SetSchemaVersion(aNewVersion)));

  return NS_OK;
}

nsresult QuotaManager::UpgradeStorageFrom0_0To1_0(
    mozIStorageConnection* aConnection) {
  AssertIsOnIOThread();
  MOZ_ASSERT(aConnection);

  const auto innerFunc = [this, &aConnection](const auto&) -> nsresult {
    QM_TRY(MOZ_TO_RESULT(UpgradeStorage<UpgradeStorageFrom0_0To1_0Helper>(
        0, MakeStorageVersion(1, 0), aConnection)));

    return NS_OK;
  };

  return ExecuteInitialization(Initialization::UpgradeStorageFrom0_0To1_0,
                               innerFunc);
}

nsresult QuotaManager::UpgradeStorageFrom1_0To2_0(
    mozIStorageConnection* aConnection) {
  AssertIsOnIOThread();
  MOZ_ASSERT(aConnection);

  // The upgrade consists of a number of logically distinct bugs that
  // intentionally got fixed at the same time to trigger just one major
  // version bump.
  //
  //
  // Morgue directory cleanup
  // [Feature/Bug]:
  // The original bug that added "on demand" morgue cleanup is 1165119.
  //
  // [Mutations]:
  // Morgue directories are removed from all origin directories during the
  // upgrade process. Origin initialization and usage calculation doesn't try
  // to remove morgue directories anymore.
  //
  // [Downgrade-incompatible changes]:
  // Morgue directories can reappear if user runs an already upgraded profile
  // in an older version of Firefox. Morgue directories then prevent current
  // Firefox from initializing and using the storage.
  //
  //
  // App data removal
  // [Feature/Bug]:
  // The bug that removes isApp flags is 1311057.
  //
  // [Mutations]:
  // Origin directories with appIds are removed during the upgrade process.
  //
  // [Downgrade-incompatible changes]:
  // Origin directories with appIds can reappear if user runs an already
  // upgraded profile in an older version of Firefox. Origin directories with
  // appIds don't prevent current Firefox from initializing and using the
  // storage, but they wouldn't ever be removed again, potentially causing
  // problems once appId is removed from origin attributes.
  //
  //
  // Strip obsolete origin attributes
  // [Feature/Bug]:
  // The bug that strips obsolete origin attributes is 1314361.
  //
  // [Mutations]:
  // Origin directories with obsolete origin attributes are renamed and their
  // metadata files are updated during the upgrade process.
  //
  // [Downgrade-incompatible changes]:
  // Origin directories with obsolete origin attributes can reappear if user
  // runs an already upgraded profile in an older version of Firefox. Origin
  // directories with obsolete origin attributes don't prevent current Firefox
  // from initializing and using the storage, but they wouldn't ever be upgraded
  // again, potentially causing problems in future.
  //
  //
  // File manager directory renaming (client specific)
  // [Feature/Bug]:
  // The original bug that added "on demand" file manager directory renaming is
  // 1056939.
  //
  // [Mutations]:
  // All file manager directories are renamed to contain the ".files" suffix.
  //
  // [Downgrade-incompatible changes]:
  // File manager directories with the ".files" suffix prevent older versions of
  // Firefox from initializing and using the storage.
  // File manager directories without the ".files" suffix can appear if user
  // runs an already upgraded profile in an older version of Firefox. File
  // manager directories without the ".files" suffix then prevent current
  // Firefox from initializing and using the storage.

  const auto innerFunc = [this, &aConnection](const auto&) -> nsresult {
    QM_TRY(MOZ_TO_RESULT(UpgradeStorage<UpgradeStorageFrom1_0To2_0Helper>(
        MakeStorageVersion(1, 0), MakeStorageVersion(2, 0), aConnection)));

    return NS_OK;
  };

  return ExecuteInitialization(Initialization::UpgradeStorageFrom1_0To2_0,
                               innerFunc);
}

nsresult QuotaManager::UpgradeStorageFrom2_0To2_1(
    mozIStorageConnection* aConnection) {
  AssertIsOnIOThread();
  MOZ_ASSERT(aConnection);

  // The upgrade is mainly to create a directory padding file in DOM Cache
  // directory to record the overall padding size of an origin.

  const auto innerFunc = [this, &aConnection](const auto&) -> nsresult {
    QM_TRY(MOZ_TO_RESULT(UpgradeStorage<UpgradeStorageFrom2_0To2_1Helper>(
        MakeStorageVersion(2, 0), MakeStorageVersion(2, 1), aConnection)));

    return NS_OK;
  };

  return ExecuteInitialization(Initialization::UpgradeStorageFrom2_0To2_1,
                               innerFunc);
}

nsresult QuotaManager::UpgradeStorageFrom2_1To2_2(
    mozIStorageConnection* aConnection) {
  AssertIsOnIOThread();
  MOZ_ASSERT(aConnection);

  // The upgrade is mainly to clean obsolete origins in the repositoies, remove
  // asmjs client, and ".tmp" file in the idb folers.

  const auto innerFunc = [this, &aConnection](const auto&) -> nsresult {
    QM_TRY(MOZ_TO_RESULT(UpgradeStorage<UpgradeStorageFrom2_1To2_2Helper>(
        MakeStorageVersion(2, 1), MakeStorageVersion(2, 2), aConnection)));

    return NS_OK;
  };

  return ExecuteInitialization(Initialization::UpgradeStorageFrom2_1To2_2,
                               innerFunc);
}

nsresult QuotaManager::UpgradeStorageFrom2_2To2_3(
    mozIStorageConnection* aConnection) {
  AssertIsOnIOThread();
  MOZ_ASSERT(aConnection);

  const auto innerFunc = [&aConnection](const auto&) -> nsresult {
    // Table `database`
    QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL(
        nsLiteralCString("CREATE TABLE database"
                         "( cache_version INTEGER NOT NULL DEFAULT 0"
                         ");"))));

    QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL(
        nsLiteralCString("INSERT INTO database (cache_version) "
                         "VALUES (0)"))));

#ifdef DEBUG
    {
      QM_TRY_INSPECT(
          const int32_t& storageVersion,
          MOZ_TO_RESULT_INVOKE_MEMBER(aConnection, GetSchemaVersion));

      MOZ_ASSERT(storageVersion == MakeStorageVersion(2, 2));
    }
#endif

    QM_TRY(
        MOZ_TO_RESULT(aConnection->SetSchemaVersion(MakeStorageVersion(2, 3))));

    return NS_OK;
  };

  return ExecuteInitialization(Initialization::UpgradeStorageFrom2_2To2_3,
                               innerFunc);
}

nsresult QuotaManager::MaybeRemoveLocalStorageDataAndArchive(
    nsIFile& aLsArchiveFile) {
  AssertIsOnIOThread();
  MOZ_ASSERT(!CachedNextGenLocalStorageEnabled());

  QM_TRY_INSPECT(const bool& exists,
                 MOZ_TO_RESULT_INVOKE_MEMBER(aLsArchiveFile, Exists));

  if (!exists) {
    // If the ls archive doesn't exist then ls directories can't exist either.
    return NS_OK;
  }

  QM_TRY(MOZ_TO_RESULT(MaybeRemoveLocalStorageDirectories()));

  InvalidateQuotaCache();

  // Finally remove the ls archive, so we don't have to check all origin
  // directories next time this method is called.
  QM_TRY(MOZ_TO_RESULT(aLsArchiveFile.Remove(false)));

  return NS_OK;
}

nsresult QuotaManager::MaybeRemoveLocalStorageDirectories() {
  AssertIsOnIOThread();

  QM_TRY_INSPECT(const auto& defaultStorageDir,
                 QM_NewLocalFile(*mDefaultStoragePath));

  QM_TRY_INSPECT(const bool& exists,
                 MOZ_TO_RESULT_INVOKE_MEMBER(defaultStorageDir, Exists));

  if (!exists) {
    return NS_OK;
  }

  QM_TRY(CollectEachFile(
      *defaultStorageDir,
      [](const nsCOMPtr<nsIFile>& originDir) -> Result<Ok, nsresult> {
#ifdef DEBUG
        {
          QM_TRY_INSPECT(const bool& exists,
                         MOZ_TO_RESULT_INVOKE_MEMBER(originDir, Exists));
          MOZ_ASSERT(exists);
        }
#endif

        QM_TRY_INSPECT(const auto& dirEntryKind, GetDirEntryKind(*originDir));

        switch (dirEntryKind) {
          case nsIFileKind::ExistsAsDirectory: {
            QM_TRY_INSPECT(
                const auto& lsDir,
                CloneFileAndAppend(*originDir, NS_LITERAL_STRING_FROM_CSTRING(
                                                   LS_DIRECTORY_NAME)));

            {
              QM_TRY_INSPECT(const bool& exists,
                             MOZ_TO_RESULT_INVOKE_MEMBER(lsDir, Exists));

              if (!exists) {
                return Ok{};
              }
            }

            {
              QM_TRY_INSPECT(const bool& isDirectory,
                             MOZ_TO_RESULT_INVOKE_MEMBER(lsDir, IsDirectory));

              if (!isDirectory) {
                QM_WARNING("ls entry is not a directory!");

                return Ok{};
              }
            }

            nsString path;
            QM_TRY(MOZ_TO_RESULT(lsDir->GetPath(path)));

            QM_WARNING("Deleting %s directory!",
                       NS_ConvertUTF16toUTF8(path).get());

            QM_TRY(MOZ_TO_RESULT(lsDir->Remove(/* aRecursive */ true)));

            break;
          }

          case nsIFileKind::ExistsAsFile: {
            QM_TRY_INSPECT(const auto& leafName,
                           MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
                               nsAutoString, originDir, GetLeafName));

            // Unknown files during upgrade are allowed. Just warn if we find
            // them.
            if (!IsOSMetadata(leafName)) {
              UNKNOWN_FILE_WARNING(leafName);
            }

            break;
          }

          case nsIFileKind::DoesNotExist:
            // Ignore files that got removed externally while iterating.
            break;
        }
        return Ok{};
      }));

  return NS_OK;
}

Result<Ok, nsresult> QuotaManager::CopyLocalStorageArchiveFromWebAppsStore(
    nsIFile& aLsArchiveFile) const {
  AssertIsOnIOThread();
  MOZ_ASSERT(CachedNextGenLocalStorageEnabled());

#ifdef DEBUG
  {
    QM_TRY_INSPECT(const bool& exists,
                   MOZ_TO_RESULT_INVOKE_MEMBER(aLsArchiveFile, Exists));
    MOZ_ASSERT(!exists);
  }
#endif

  // Get the storage service first, we will need it at multiple places.
  QM_TRY_INSPECT(const auto& ss,
                 MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<mozIStorageService>,
                                         MOZ_SELECT_OVERLOAD(do_GetService),
                                         MOZ_STORAGE_SERVICE_CONTRACTID));

  // Get the web apps store file.
  QM_TRY_INSPECT(const auto& webAppsStoreFile, QM_NewLocalFile(mBasePath));

  QM_TRY(MOZ_TO_RESULT(
      webAppsStoreFile->Append(nsLiteralString(WEB_APPS_STORE_FILE_NAME))));

  // Now check if the web apps store is useable.
  QM_TRY_INSPECT(const auto& connection,
                 CreateWebAppsStoreConnection(*webAppsStoreFile, *ss));

  if (connection) {
    // Find out the journal mode.
    QM_TRY_INSPECT(const auto& stmt,
                   CreateAndExecuteSingleStepStatement(
                       *connection, "PRAGMA journal_mode;"_ns));

    QM_TRY_INSPECT(const auto& journalMode,
                   MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoCString, *stmt,
                                                     GetUTF8String, 0));

    QM_TRY(MOZ_TO_RESULT(stmt->Finalize()));

    if (journalMode.EqualsLiteral("wal")) {
      // We don't copy the WAL file, so make sure the old database is fully
      // checkpointed.
      QM_TRY(MOZ_TO_RESULT(
          connection->ExecuteSimpleSQL("PRAGMA wal_checkpoint(TRUNCATE);"_ns)));
    }

    // Explicitely close the connection before the old database is copied.
    QM_TRY(MOZ_TO_RESULT(connection->Close()));

    // Copy the old database. The database is copied from
    // <profile>/webappsstore.sqlite to
    // <profile>/storage/ls-archive-tmp.sqlite
    // We use a "-tmp" postfix since we are not done yet.
    QM_TRY_INSPECT(const auto& storageDir, QM_NewLocalFile(*mStoragePath));

    QM_TRY(MOZ_TO_RESULT(webAppsStoreFile->CopyTo(
        storageDir, nsLiteralString(LS_ARCHIVE_TMP_FILE_NAME))));

    QM_TRY_INSPECT(const auto& lsArchiveTmpFile,
                   GetLocalStorageArchiveTmpFile(*mStoragePath));

    if (journalMode.EqualsLiteral("wal")) {
      QM_TRY_INSPECT(
          const auto& lsArchiveTmpConnection,
          MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
              nsCOMPtr<mozIStorageConnection>, ss, OpenUnsharedDatabase,
              lsArchiveTmpFile, mozIStorageService::CONNECTION_DEFAULT));

      // The archive will only be used for lazy data migration. There won't be
      // any concurrent readers and writers that could benefit from Write-Ahead
      // Logging. So switch to a standard rollback journal. The standard
      // rollback journal also provides atomicity across multiple attached
      // databases which is import for the lazy data migration to work safely.
      QM_TRY(MOZ_TO_RESULT(lsArchiveTmpConnection->ExecuteSimpleSQL(
          "PRAGMA journal_mode = DELETE;"_ns)));

      // Close the connection explicitly. We are going to rename the file below.
      QM_TRY(MOZ_TO_RESULT(lsArchiveTmpConnection->Close()));
    }

    // Finally, rename ls-archive-tmp.sqlite to ls-archive.sqlite
    QM_TRY(MOZ_TO_RESULT(lsArchiveTmpFile->MoveTo(
        nullptr, nsLiteralString(LS_ARCHIVE_FILE_NAME))));

    return Ok{};
  }

  // If webappsstore database is not useable, just create an empty archive.
  // XXX The code below should be removed and the caller should call us only
  // when webappstore.sqlite exists. CreateWebAppsStoreConnection should be
  // reworked to propagate database corruption instead of returning null
  // connection.
  // So, if there's no webappsstore.sqlite
  // MaybeCreateOrUpgradeLocalStorageArchive will call
  // CreateEmptyLocalStorageArchive instead of
  // CopyLocalStorageArchiveFromWebAppsStore.
  // If there's any corruption detected during
  // MaybeCreateOrUpgradeLocalStorageArchive (including nested calls like
  // CopyLocalStorageArchiveFromWebAppsStore and CreateWebAppsStoreConnection)
  // EnsureStorageIsInitialized will fallback to
  // CreateEmptyLocalStorageArchive.

  // Ensure the storage directory actually exists.
  QM_TRY_INSPECT(const auto& storageDirectory, QM_NewLocalFile(*mStoragePath));

  QM_TRY_INSPECT(const bool& created, EnsureDirectory(*storageDirectory));

  Unused << created;

  QM_TRY_UNWRAP(auto lsArchiveConnection,
                MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
                    nsCOMPtr<mozIStorageConnection>, ss, OpenUnsharedDatabase,
                    &aLsArchiveFile, mozIStorageService::CONNECTION_DEFAULT));

  QM_TRY(MOZ_TO_RESULT(
      StorageDBUpdater::CreateCurrentSchema(lsArchiveConnection)));

  return Ok{};
}

Result<nsCOMPtr<mozIStorageConnection>, nsresult>
QuotaManager::CreateLocalStorageArchiveConnection(
    nsIFile& aLsArchiveFile) const {
  AssertIsOnIOThread();
  MOZ_ASSERT(CachedNextGenLocalStorageEnabled());

#ifdef DEBUG
  {
    QM_TRY_INSPECT(const bool& exists,
                   MOZ_TO_RESULT_INVOKE_MEMBER(aLsArchiveFile, Exists));
    MOZ_ASSERT(exists);
  }
#endif

  QM_TRY_INSPECT(const bool& isDirectory,
                 MOZ_TO_RESULT_INVOKE_MEMBER(aLsArchiveFile, IsDirectory));

  // A directory with the name of the archive file is treated as corruption
  // (similarly as wrong content of the file).
  QM_TRY(OkIf(!isDirectory), Err(NS_ERROR_FILE_CORRUPTED));

  QM_TRY_INSPECT(const auto& ss,
                 MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<mozIStorageService>,
                                         MOZ_SELECT_OVERLOAD(do_GetService),
                                         MOZ_STORAGE_SERVICE_CONTRACTID));

  // This may return NS_ERROR_FILE_CORRUPTED too.
  QM_TRY_UNWRAP(auto connection,
                MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
                    nsCOMPtr<mozIStorageConnection>, ss, OpenUnsharedDatabase,
                    &aLsArchiveFile, mozIStorageService::CONNECTION_DEFAULT));

  // The legacy LS implementation removes the database and creates an empty one
  // when the schema can't be updated. The same effect can be achieved here by
  // mapping all errors to NS_ERROR_FILE_CORRUPTED. One such case is tested by
  // sub test case 3 of dom/localstorage/test/unit/test_archive.js
  QM_TRY(
      MOZ_TO_RESULT(StorageDBUpdater::Update(connection))
          .mapErr([](const nsresult rv) { return NS_ERROR_FILE_CORRUPTED; }));

  return connection;
}

Result<nsCOMPtr<mozIStorageConnection>, nsresult>
QuotaManager::RecopyLocalStorageArchiveFromWebAppsStore(
    nsIFile& aLsArchiveFile) {
  AssertIsOnIOThread();
  MOZ_ASSERT(CachedNextGenLocalStorageEnabled());

  QM_TRY(MOZ_TO_RESULT(MaybeRemoveLocalStorageDirectories()));

#ifdef DEBUG
  {
    QM_TRY_INSPECT(const bool& exists,
                   MOZ_TO_RESULT_INVOKE_MEMBER(aLsArchiveFile, Exists));

    MOZ_ASSERT(exists);
  }
#endif

  QM_TRY(MOZ_TO_RESULT(aLsArchiveFile.Remove(false)));

  QM_TRY(CopyLocalStorageArchiveFromWebAppsStore(aLsArchiveFile));

  QM_TRY_UNWRAP(auto connection,
                CreateLocalStorageArchiveConnection(aLsArchiveFile));

  QM_TRY(MOZ_TO_RESULT(InitializeLocalStorageArchive(connection)));

  return connection;
}

Result<nsCOMPtr<mozIStorageConnection>, nsresult>
QuotaManager::DowngradeLocalStorageArchive(nsIFile& aLsArchiveFile) {
  AssertIsOnIOThread();
  MOZ_ASSERT(CachedNextGenLocalStorageEnabled());

  QM_TRY_UNWRAP(auto connection,
                RecopyLocalStorageArchiveFromWebAppsStore(aLsArchiveFile));

  QM_TRY(MOZ_TO_RESULT(
      SaveLocalStorageArchiveVersion(connection, kLocalStorageArchiveVersion)));

  return connection;
}

Result<nsCOMPtr<mozIStorageConnection>, nsresult>
QuotaManager::UpgradeLocalStorageArchiveFromLessThan4To4(
    nsIFile& aLsArchiveFile) {
  AssertIsOnIOThread();
  MOZ_ASSERT(CachedNextGenLocalStorageEnabled());

  QM_TRY_UNWRAP(auto connection,
                RecopyLocalStorageArchiveFromWebAppsStore(aLsArchiveFile));

  QM_TRY(MOZ_TO_RESULT(SaveLocalStorageArchiveVersion(connection, 4)));

  return connection;
}

/*
nsresult QuotaManager::UpgradeLocalStorageArchiveFrom4To5(
    nsCOMPtr<mozIStorageConnection>& aConnection) {
  AssertIsOnIOThread();
  MOZ_ASSERT(CachedNextGenLocalStorageEnabled());

  nsresult rv = SaveLocalStorageArchiveVersion(aConnection, 5);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}
*/

#ifdef DEBUG

void QuotaManager::AssertStorageIsInitialized() const {
  AssertIsOnIOThread();
  MOZ_ASSERT(IsStorageInitialized());
}

#endif  // DEBUG

nsresult QuotaManager::MaybeUpgradeToDefaultStorageDirectory(
    nsIFile& aStorageFile) {
  AssertIsOnIOThread();

  QM_TRY_INSPECT(const auto& storageFileExists,
                 MOZ_TO_RESULT_INVOKE_MEMBER(aStorageFile, Exists));

  if (!storageFileExists) {
    QM_TRY_INSPECT(const auto& indexedDBDir, QM_NewLocalFile(*mIndexedDBPath));

    QM_TRY_INSPECT(const auto& indexedDBDirExists,
                   MOZ_TO_RESULT_INVOKE_MEMBER(indexedDBDir, Exists));

    if (indexedDBDirExists) {
      QM_TRY(MOZ_TO_RESULT(
          UpgradeFromIndexedDBDirectoryToPersistentStorageDirectory(
              indexedDBDir)));
    }

    QM_TRY_INSPECT(const auto& persistentStorageDir,
                   QM_NewLocalFile(*mStoragePath));

    QM_TRY(MOZ_TO_RESULT(persistentStorageDir->Append(
        nsLiteralString(PERSISTENT_DIRECTORY_NAME))));

    QM_TRY_INSPECT(const auto& persistentStorageDirExists,
                   MOZ_TO_RESULT_INVOKE_MEMBER(persistentStorageDir, Exists));

    if (persistentStorageDirExists) {
      QM_TRY(MOZ_TO_RESULT(
          UpgradeFromPersistentStorageDirectoryToDefaultStorageDirectory(
              persistentStorageDir)));
    }
  }

  return NS_OK;
}

nsresult QuotaManager::MaybeCreateOrUpgradeStorage(
    mozIStorageConnection& aConnection) {
  AssertIsOnIOThread();

  QM_TRY_UNWRAP(auto storageVersion,
                MOZ_TO_RESULT_INVOKE_MEMBER(aConnection, GetSchemaVersion));

  // Hacky downgrade logic!
  // If we see major.minor of 3.0, downgrade it to be 2.1.
  if (storageVersion == kHackyPreDowngradeStorageVersion) {
    storageVersion = kHackyPostDowngradeStorageVersion;
    QM_TRY(MOZ_TO_RESULT(aConnection.SetSchemaVersion(storageVersion)),
           QM_PROPAGATE,
           [](const auto&) { MOZ_ASSERT(false, "Downgrade didn't take."); });
  }

  QM_TRY(OkIf(GetMajorStorageVersion(storageVersion) <= kMajorStorageVersion),
         NS_ERROR_FAILURE, [](const auto&) {
           NS_WARNING("Unable to initialize storage, version is too high!");
         });

  if (storageVersion < kStorageVersion) {
    const bool newDatabase = !storageVersion;

    QM_TRY_INSPECT(const auto& storageDir, QM_NewLocalFile(*mStoragePath));

    QM_TRY_INSPECT(const auto& storageDirExists,
                   MOZ_TO_RESULT_INVOKE_MEMBER(storageDir, Exists));

    const bool newDirectory = !storageDirExists;

    if (newDatabase) {
      // Set the page size first.
      if (kSQLitePageSizeOverride) {
        QM_TRY(MOZ_TO_RESULT(aConnection.ExecuteSimpleSQL(nsPrintfCString(
            "PRAGMA page_size = %" PRIu32 ";", kSQLitePageSizeOverride))));
      }
    }

    mozStorageTransaction transaction(
        &aConnection, false, mozIStorageConnection::TRANSACTION_IMMEDIATE);

    QM_TRY(MOZ_TO_RESULT(transaction.Start()));

    // An upgrade method can upgrade the database, the storage or both.
    // The upgrade loop below can only be avoided when there's no database and
    // no storage yet (e.g. new profile).
    if (newDatabase && newDirectory) {
      QM_TRY(MOZ_TO_RESULT(CreateTables(&aConnection)));

#ifdef DEBUG
      {
        QM_TRY_INSPECT(
            const int32_t& storageVersion,
            MOZ_TO_RESULT_INVOKE_MEMBER(aConnection, GetSchemaVersion),
            QM_ASSERT_UNREACHABLE);
        MOZ_ASSERT(storageVersion == kStorageVersion);
      }
#endif

      QM_TRY(MOZ_TO_RESULT(aConnection.ExecuteSimpleSQL(
          nsLiteralCString("INSERT INTO database (cache_version) "
                           "VALUES (0)"))));
    } else {
      // This logic needs to change next time we change the storage!
      static_assert(kStorageVersion == int32_t((2 << 16) + 3),
                    "Upgrade function needed due to storage version increase.");

      while (storageVersion != kStorageVersion) {
        if (storageVersion == 0) {
          QM_TRY(MOZ_TO_RESULT(UpgradeStorageFrom0_0To1_0(&aConnection)));
        } else if (storageVersion == MakeStorageVersion(1, 0)) {
          QM_TRY(MOZ_TO_RESULT(UpgradeStorageFrom1_0To2_0(&aConnection)));
        } else if (storageVersion == MakeStorageVersion(2, 0)) {
          QM_TRY(MOZ_TO_RESULT(UpgradeStorageFrom2_0To2_1(&aConnection)));
        } else if (storageVersion == MakeStorageVersion(2, 1)) {
          QM_TRY(MOZ_TO_RESULT(UpgradeStorageFrom2_1To2_2(&aConnection)));
        } else if (storageVersion == MakeStorageVersion(2, 2)) {
          QM_TRY(MOZ_TO_RESULT(UpgradeStorageFrom2_2To2_3(&aConnection)));
        } else {
          QM_FAIL(NS_ERROR_FAILURE, []() {
            NS_WARNING(
                "Unable to initialize storage, no upgrade path is "
                "available!");
          });
        }

        QM_TRY_UNWRAP(storageVersion, MOZ_TO_RESULT_INVOKE_MEMBER(
                                          aConnection, GetSchemaVersion));
      }

      MOZ_ASSERT(storageVersion == kStorageVersion);
    }

    QM_TRY(MOZ_TO_RESULT(transaction.Commit()));
  }

  return NS_OK;
}

OkOrErr QuotaManager::MaybeRemoveLocalStorageArchiveTmpFile() {
  AssertIsOnIOThread();

  QM_TRY_INSPECT(
      const auto& lsArchiveTmpFile,
      QM_TO_RESULT_TRANSFORM(GetLocalStorageArchiveTmpFile(*mStoragePath)));

  QM_TRY_INSPECT(const bool& exists,
                 QM_TO_RESULT_INVOKE_MEMBER(lsArchiveTmpFile, Exists));

  if (exists) {
    QM_TRY(QM_TO_RESULT(lsArchiveTmpFile->Remove(false)));
  }

  return Ok{};
}

Result<Ok, nsresult> QuotaManager::MaybeCreateOrUpgradeLocalStorageArchive(
    nsIFile& aLsArchiveFile) {
  AssertIsOnIOThread();

  QM_TRY_INSPECT(
      const bool& lsArchiveFileExisted,
      ([this, &aLsArchiveFile]() -> Result<bool, nsresult> {
        QM_TRY_INSPECT(const bool& exists,
                       MOZ_TO_RESULT_INVOKE_MEMBER(aLsArchiveFile, Exists));

        if (!exists) {
          QM_TRY(CopyLocalStorageArchiveFromWebAppsStore(aLsArchiveFile));
        }

        return exists;
      }()));

  QM_TRY_UNWRAP(auto connection,
                CreateLocalStorageArchiveConnection(aLsArchiveFile));

  QM_TRY_INSPECT(const auto& initialized,
                 IsLocalStorageArchiveInitialized(*connection));

  if (!initialized) {
    QM_TRY(MOZ_TO_RESULT(InitializeLocalStorageArchive(connection)));
  }

  QM_TRY_UNWRAP(int32_t version, LoadLocalStorageArchiveVersion(*connection));

  if (version > kLocalStorageArchiveVersion) {
    // Close local storage archive connection. We are going to remove underlying
    // file.
    QM_TRY(MOZ_TO_RESULT(connection->Close()));

    // This will wipe the archive and any migrated data and recopy the archive
    // from webappsstore.sqlite.
    QM_TRY_UNWRAP(connection, DowngradeLocalStorageArchive(aLsArchiveFile));

    QM_TRY_UNWRAP(version, LoadLocalStorageArchiveVersion(*connection));

    MOZ_ASSERT(version == kLocalStorageArchiveVersion);
  } else if (version != kLocalStorageArchiveVersion) {
    // The version can be zero either when the archive didn't exist or it did
    // exist, but the archive was created without any version information.
    // We don't need to do any upgrades only if it didn't exist because existing
    // archives without version information must be recopied to really fix bug
    // 1542104. See also bug 1546305 which introduced archive versions.
    if (!lsArchiveFileExisted) {
      MOZ_ASSERT(version == 0);

      QM_TRY(MOZ_TO_RESULT(SaveLocalStorageArchiveVersion(
          connection, kLocalStorageArchiveVersion)));
    } else {
      static_assert(kLocalStorageArchiveVersion == 4,
                    "Upgrade function needed due to LocalStorage archive "
                    "version increase.");

      while (version != kLocalStorageArchiveVersion) {
        if (version < 4) {
          // Close local storage archive connection. We are going to remove
          // underlying file.
          QM_TRY(MOZ_TO_RESULT(connection->Close()));

          // This won't do an "upgrade" in a normal sense. It will wipe the
          // archive and any migrated data and recopy the archive from
          // webappsstore.sqlite
          QM_TRY_UNWRAP(connection, UpgradeLocalStorageArchiveFromLessThan4To4(
                                        aLsArchiveFile));
        } /* else if (version == 4) {
          QM_TRY(MOZ_TO_RESULT(UpgradeLocalStorageArchiveFrom4To5(connection)));
        } */
        else {
          QM_FAIL(Err(NS_ERROR_FAILURE), []() {
            QM_WARNING(
                "Unable to initialize LocalStorage archive, no upgrade path "
                "is available!");
          });
        }

        QM_TRY_UNWRAP(version, LoadLocalStorageArchiveVersion(*connection));
      }

      MOZ_ASSERT(version == kLocalStorageArchiveVersion);
    }
  }

  // At this point, we have finished initializing the local storage archive, and
  // can continue storage initialization. We don't know though if the actual
  // data in the archive file is readable. We can't do a PRAGMA integrity_check
  // here though, because that would be too heavyweight.

  return Ok{};
}

Result<Ok, nsresult> QuotaManager::CreateEmptyLocalStorageArchive(
    nsIFile& aLsArchiveFile) const {
  AssertIsOnIOThread();

  QM_TRY_INSPECT(const bool& exists,
                 MOZ_TO_RESULT_INVOKE_MEMBER(aLsArchiveFile, Exists));

  // If it exists, remove it. It might be a directory, so remove it recursively.
  if (exists) {
    QM_TRY(MOZ_TO_RESULT(aLsArchiveFile.Remove(true)));

    // XXX If we crash right here, the next session will copy the archive from
    // webappsstore.sqlite again!
    // XXX Create a marker file before removing the archive which can be
    // used in MaybeCreateOrUpgradeLocalStorageArchive to create an empty
    // archive instead of recopying it from webapppstore.sqlite (in other
    // words, finishing what was started here).
  }

  QM_TRY_INSPECT(const auto& ss,
                 MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<mozIStorageService>,
                                         MOZ_SELECT_OVERLOAD(do_GetService),
                                         MOZ_STORAGE_SERVICE_CONTRACTID));

  QM_TRY_UNWRAP(const auto connection,
                MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
                    nsCOMPtr<mozIStorageConnection>, ss, OpenUnsharedDatabase,
                    &aLsArchiveFile, mozIStorageService::CONNECTION_DEFAULT));

  QM_TRY(MOZ_TO_RESULT(StorageDBUpdater::CreateCurrentSchema(connection)));

  QM_TRY(MOZ_TO_RESULT(InitializeLocalStorageArchive(connection)));

  QM_TRY(MOZ_TO_RESULT(
      SaveLocalStorageArchiveVersion(connection, kLocalStorageArchiveVersion)));

  return Ok{};
}

nsresult QuotaManager::EnsureStorageIsInitialized() {
  DiagnosticAssertIsOnIOThread();

  const auto innerFunc =
      [&](const auto& firstInitializationAttempt) -> nsresult {
    if (mStorageConnection) {
      MOZ_ASSERT(firstInitializationAttempt.Recorded());
      return NS_OK;
    }

    QM_TRY_INSPECT(const auto& storageFile, QM_NewLocalFile(mBasePath));
    QM_TRY(MOZ_TO_RESULT(storageFile->Append(mStorageName + kSQLiteSuffix)));

    QM_TRY(MOZ_TO_RESULT(MaybeUpgradeToDefaultStorageDirectory(*storageFile)));

    QM_TRY_INSPECT(const auto& ss,
                   MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<mozIStorageService>,
                                           MOZ_SELECT_OVERLOAD(do_GetService),
                                           MOZ_STORAGE_SERVICE_CONTRACTID));

    QM_TRY_UNWRAP(
        auto connection,
        QM_OR_ELSE_WARN_IF(
            // Expression.
            MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
                nsCOMPtr<mozIStorageConnection>, ss, OpenUnsharedDatabase,
                storageFile, mozIStorageService::CONNECTION_DEFAULT),
            // Predicate.
            IsDatabaseCorruptionError,
            // Fallback.
            ErrToDefaultOk<nsCOMPtr<mozIStorageConnection>>));

    if (!connection) {
      // Nuke the database file.
      QM_TRY(MOZ_TO_RESULT(storageFile->Remove(false)));

      QM_TRY_UNWRAP(connection, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
                                    nsCOMPtr<mozIStorageConnection>, ss,
                                    OpenUnsharedDatabase, storageFile,
                                    mozIStorageService::CONNECTION_DEFAULT));
    }

    // We want extra durability for this important file.
    QM_TRY(MOZ_TO_RESULT(
        connection->ExecuteSimpleSQL("PRAGMA synchronous = EXTRA;"_ns)));

    // Check to make sure that the storage version is correct.
    QM_TRY(MOZ_TO_RESULT(MaybeCreateOrUpgradeStorage(*connection)));

    QM_TRY(MaybeRemoveLocalStorageArchiveTmpFile());

    QM_TRY_INSPECT(const auto& lsArchiveFile,
                   GetLocalStorageArchiveFile(*mStoragePath));

    if (CachedNextGenLocalStorageEnabled()) {
      QM_TRY(QM_OR_ELSE_WARN_IF(
          // Expression.
          MaybeCreateOrUpgradeLocalStorageArchive(*lsArchiveFile),
          // Predicate.
          IsDatabaseCorruptionError,
          // Fallback.
          ([&](const nsresult rv) -> Result<Ok, nsresult> {
            QM_TRY_RETURN(CreateEmptyLocalStorageArchive(*lsArchiveFile));
          })));
    } else {
      QM_TRY(
          MOZ_TO_RESULT(MaybeRemoveLocalStorageDataAndArchive(*lsArchiveFile)));
    }

    QM_TRY_UNWRAP(mCacheUsable, MaybeCreateOrUpgradeCache(*connection));

    if (mCacheUsable && gInvalidateQuotaCache) {
      QM_TRY(InvalidateCache(*connection));

      gInvalidateQuotaCache = false;
    }

    mStorageConnection = std::move(connection);

    return NS_OK;
  };

  return ExecuteInitialization(
      Initialization::Storage,
      "dom::quota::FirstInitializationAttempt::Storage"_ns, innerFunc);
}

RefPtr<ClientDirectoryLock> QuotaManager::CreateDirectoryLock(
    PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata,
    Client::Type aClientType, bool aExclusive) {
  AssertIsOnOwningThread();

  return DirectoryLockImpl::Create(WrapNotNullUnchecked(this), aPersistenceType,
                                   aOriginMetadata, aClientType, aExclusive);
}

RefPtr<UniversalDirectoryLock> QuotaManager::CreateDirectoryLockInternal(
    const Nullable<PersistenceType>& aPersistenceType,
    const OriginScope& aOriginScope, const Nullable<Client::Type>& aClientType,
    bool aExclusive) {
  AssertIsOnOwningThread();

  return DirectoryLockImpl::CreateInternal(WrapNotNullUnchecked(this),
                                           aPersistenceType, aOriginScope,
                                           aClientType, aExclusive);
}

Result<std::pair<nsCOMPtr<nsIFile>, bool>, nsresult>
QuotaManager::EnsurePersistentOriginIsInitialized(
    const OriginMetadata& aOriginMetadata) {
  AssertIsOnIOThread();
  MOZ_ASSERT(aOriginMetadata.mPersistenceType == PERSISTENCE_TYPE_PERSISTENT);
  MOZ_DIAGNOSTIC_ASSERT(mStorageConnection);

  const auto innerFunc = [&aOriginMetadata,
                          this](const auto& firstInitializationAttempt)
      -> mozilla::Result<std::pair<nsCOMPtr<nsIFile>, bool>, nsresult> {
    QM_TRY_UNWRAP(auto directory, GetOriginDirectory(aOriginMetadata));

    if (mInitializedOrigins.Contains(aOriginMetadata.mOrigin)) {
      MOZ_ASSERT(firstInitializationAttempt.Recorded());
      return std::pair(std::move(directory), false);
    }

    QM_TRY_INSPECT(const bool& created, EnsureOriginDirectory(*directory));

    QM_TRY_INSPECT(
        const int64_t& timestamp,
        ([this, created, &directory,
          &aOriginMetadata]() -> Result<int64_t, nsresult> {
          if (created) {
            const int64_t timestamp = PR_Now();

            // Only creating .metadata-v2 to reduce IO.
            QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata2(*directory, timestamp,
                                                          /* aPersisted */ true,
                                                          aOriginMetadata)));

            return timestamp;
          }

          // Get the metadata. We only use the timestamp.
          QM_TRY_INSPECT(const auto& metadata,
                         LoadFullOriginMetadataWithRestore(directory));

          MOZ_ASSERT(metadata.mLastAccessTime <= PR_Now());

          return metadata.mLastAccessTime;
        }()));

    QM_TRY(MOZ_TO_RESULT(InitializeOrigin(PERSISTENCE_TYPE_PERSISTENT,
                                          aOriginMetadata, timestamp,
                                          /* aPersisted */ true, directory)));

    mInitializedOrigins.AppendElement(aOriginMetadata.mOrigin);

    return std::pair(std::move(directory), created);
  };

  return ExecuteOriginInitialization(
      aOriginMetadata.mOrigin, OriginInitialization::PersistentOrigin,
      "dom::quota::FirstOriginInitializationAttempt::PersistentOrigin"_ns,
      innerFunc);
}

Result<std::pair<nsCOMPtr<nsIFile>, bool>, nsresult>
QuotaManager::EnsureTemporaryOriginIsInitialized(
    PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata) {
  AssertIsOnIOThread();
  MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT);
  MOZ_ASSERT(aOriginMetadata.mPersistenceType == aPersistenceType);
  MOZ_DIAGNOSTIC_ASSERT(mStorageConnection);
  MOZ_DIAGNOSTIC_ASSERT(mTemporaryStorageInitialized);

  const auto innerFunc = [&aOriginMetadata, this](const auto&)
      -> mozilla::Result<std::pair<nsCOMPtr<nsIFile>, bool>, nsresult> {
    // Get directory for this origin and persistence type.
    QM_TRY_UNWRAP(auto directory, GetOriginDirectory(aOriginMetadata));

    QM_TRY_INSPECT(const bool& created, EnsureOriginDirectory(*directory));

    if (created) {
      const int64_t timestamp =
          NoteOriginDirectoryCreated(aOriginMetadata, /* aPersisted */ false);

      // Only creating .metadata-v2 to reduce IO.
      QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata2(*directory, timestamp,
                                                    /* aPersisted */ false,
                                                    aOriginMetadata)));
    }

    // TODO: If the metadata file exists and we didn't call
    //       LoadFullOriginMetadataWithRestore for it (because the quota info
    //       was loaded from the cache), then the group in the metadata file
    //       may be wrong, so it should be checked and eventually updated.
    //       It's not a big deal that we are not doing it here, because the
    //       origin will be marked as "accessed", so
    //       LoadFullOriginMetadataWithRestore will be called for the metadata
    //       file in next session in LoadQuotaFromCache.

    return std::pair(std::move(directory), created);
  };

  return ExecuteOriginInitialization(
      aOriginMetadata.mOrigin, OriginInitialization::TemporaryOrigin,
      "dom::quota::FirstOriginInitializationAttempt::TemporaryOrigin"_ns,
      innerFunc);
}

nsresult QuotaManager::EnsureTemporaryStorageIsInitialized() {
  AssertIsOnIOThread();
  MOZ_DIAGNOSTIC_ASSERT(mStorageConnection);

  const auto innerFunc =
      [&](const auto& firstInitializationAttempt) -> nsresult {
    if (mTemporaryStorageInitialized) {
      MOZ_ASSERT(firstInitializationAttempt.Recorded());
      return NS_OK;
    }

    QM_TRY_INSPECT(
        const auto& storageDir,
        MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<nsIFile>,
                                MOZ_SELECT_OVERLOAD(do_CreateInstance),
                                NS_LOCAL_FILE_CONTRACTID));

    QM_TRY(MOZ_TO_RESULT(storageDir->InitWithPath(GetStoragePath())));

    // The storage directory must exist before calling GetTemporaryStorageLimit.
    QM_TRY_INSPECT(const bool& created, EnsureDirectory(*storageDir));

    Unused << created;

    QM_TRY_UNWRAP(mTemporaryStorageLimit,
                  GetTemporaryStorageLimit(*storageDir));

    QM_TRY(MOZ_TO_RESULT(LoadQuota()));

    mTemporaryStorageInitialized = true;

    CleanupTemporaryStorage();

    if (mCacheUsable) {
      QM_TRY(InvalidateCache(*mStorageConnection));
    }

    return NS_OK;
  };

  return ExecuteInitialization(
      Initialization::TemporaryStorage,
      "dom::quota::FirstInitializationAttempt::TemporaryStorage"_ns, innerFunc);
}

RefPtr<BoolPromise> QuotaManager::ClearPrivateRepository() {
  auto clearPrivateRepositoryOp = CreateClearPrivateRepositoryOp();

  RegisterNormalOriginOp(*clearPrivateRepositoryOp);

  clearPrivateRepositoryOp->RunImmediately();

  return clearPrivateRepositoryOp->OnResults();
}

RefPtr<BoolPromise> QuotaManager::ShutdownStorage() {
  if (!mShuttingDownStorage) {
    mShuttingDownStorage = true;

    auto shutdownStorageOp = CreateShutdownStorageOp();

    RegisterNormalOriginOp(*shutdownStorageOp);

    shutdownStorageOp->RunImmediately();

    shutdownStorageOp->OnResults()->Then(
        GetCurrentSerialEventTarget(), __func__,
        [self = RefPtr<QuotaManager>(this)](bool aResolveValue) {
          self->mShuttingDownStorage = false;

          self->mShutdownStoragePromiseHolder.ResolveIfExists(aResolveValue,
                                                              __func__);
        },
        [self = RefPtr<QuotaManager>(this)](nsresult aRejectValue) {
          self->mShuttingDownStorage = false;

          self->mShutdownStoragePromiseHolder.RejectIfExists(aRejectValue,
                                                             __func__);
        });
  }

  return mShutdownStoragePromiseHolder.Ensure(__func__);
}

void QuotaManager::ShutdownStorageInternal() {
  AssertIsOnIOThread();

  if (mStorageConnection) {
    mInitializationInfo.ResetOriginInitializationInfos();
    mInitializedOrigins.Clear();

    if (mTemporaryStorageInitialized) {
      if (mCacheUsable) {
        UnloadQuota();
      } else {
        RemoveQuota();
      }

      mTemporaryStorageInitialized = false;
    }

    ReleaseIOThreadObjects();

    mStorageConnection = nullptr;
    mCacheUsable = false;
  }

  mInitializationInfo.ResetFirstInitializationAttempts();
}

Result<bool, nsresult> QuotaManager::EnsureOriginDirectory(
    nsIFile& aDirectory) {
  AssertIsOnIOThread();

  QM_TRY_INSPECT(const bool& exists,
                 MOZ_TO_RESULT_INVOKE_MEMBER(aDirectory, Exists));

  if (!exists) {
    QM_TRY_INSPECT(
        const auto& leafName,
        MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, aDirectory, GetLeafName)
            .map([](const auto& leafName) {
              return NS_ConvertUTF16toUTF8(leafName);
            }));

    QM_TRY(OkIf(IsSanitizedOriginValid(leafName)), Err(NS_ERROR_FAILURE),
           [](const auto&) {
             QM_WARNING(
                 "Preventing creation of a new origin directory which is not "
                 "supported by our origin parser or is obsolete!");
           });
  }

  QM_TRY_RETURN(EnsureDirectory(aDirectory));
}

nsresult QuotaManager::AboutToClearOrigins(
    const Nullable<PersistenceType>& aPersistenceType,
    const OriginScope& aOriginScope,
    const Nullable<Client::Type>& aClientType) {
  AssertIsOnIOThread();

  if (aClientType.IsNull()) {
    for (Client::Type type : AllClientTypes()) {
      QM_TRY(MOZ_TO_RESULT((*mClients)[type]->AboutToClearOrigins(
          aPersistenceType, aOriginScope)));
    }
  } else {
    QM_TRY(MOZ_TO_RESULT((*mClients)[aClientType.Value()]->AboutToClearOrigins(
        aPersistenceType, aOriginScope)));
  }

  return NS_OK;
}

void QuotaManager::OriginClearCompleted(
    PersistenceType aPersistenceType, const nsACString& aOrigin,
    const Nullable<Client::Type>& aClientType) {
  AssertIsOnIOThread();

  if (aClientType.IsNull()) {
    if (aPersistenceType == PERSISTENCE_TYPE_PERSISTENT) {
      mInitializedOrigins.RemoveElement(aOrigin);
    }

    for (Client::Type type : AllClientTypes()) {
      (*mClients)[type]->OnOriginClearCompleted(aPersistenceType, aOrigin);
    }
  } else {
    (*mClients)[aClientType.Value()]->OnOriginClearCompleted(aPersistenceType,
                                                             aOrigin);
  }
}

void QuotaManager::RepositoryClearCompleted(PersistenceType aPersistenceType) {
  AssertIsOnIOThread();

  if (aPersistenceType == PERSISTENCE_TYPE_PERSISTENT) {
    mInitializedOrigins.Clear();
  }

  for (Client::Type type : AllClientTypes()) {
    (*mClients)[type]->OnRepositoryClearCompleted(aPersistenceType);
  }
}

Client* QuotaManager::GetClient(Client::Type aClientType) {
  MOZ_ASSERT(aClientType >= Client::IDB);
  MOZ_ASSERT(aClientType < Client::TypeMax());

  return (*mClients)[aClientType];
}

const AutoTArray<Client::Type, Client::TYPE_MAX>&
QuotaManager::AllClientTypes() {
  if (CachedNextGenLocalStorageEnabled()) {
    return *mAllClientTypes;
  }
  return *mAllClientTypesExceptLS;
}

uint64_t QuotaManager::GetGroupLimit() const {
  // To avoid one group evicting all the rest, limit the amount any one group
  // can use to 20% resp. a fifth. To prevent individual sites from using
  // exorbitant amounts of storage where there is a lot of free space, cap the
  // group limit to 10GB.
  const auto x = std::min<uint64_t>(mTemporaryStorageLimit / 5, 10 GB);

  // In low-storage situations, make an exception (while not exceeding the total
  // storage limit).
  return std::min<uint64_t>(mTemporaryStorageLimit,
                            std::max<uint64_t>(x, 10 MB));
}

std::pair<uint64_t, uint64_t> QuotaManager::GetUsageAndLimitForEstimate(
    const OriginMetadata& aOriginMetadata) {
  AssertIsOnIOThread();

  uint64_t totalGroupUsage = 0;

  {
    MutexAutoLock lock(mQuotaMutex);

    GroupInfoPair* pair;
    if (mGroupInfoPairs.Get(aOriginMetadata.mGroup, &pair)) {
      for (const PersistenceType type : kBestEffortPersistenceTypes) {
        RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(type);
        if (groupInfo) {
          if (type == PERSISTENCE_TYPE_DEFAULT) {
            RefPtr<OriginInfo> originInfo =
                groupInfo->LockedGetOriginInfo(aOriginMetadata.mOrigin);

            if (originInfo && originInfo->LockedPersisted()) {
              return std::pair(mTemporaryStorageUsage, mTemporaryStorageLimit);
            }
          }

          AssertNoOverflow(totalGroupUsage, groupInfo->mUsage);
          totalGroupUsage += groupInfo->mUsage;
        }
      }
    }
  }

  return std::pair(totalGroupUsage, GetGroupLimit());
}

uint64_t QuotaManager::GetOriginUsage(
    const PrincipalMetadata& aPrincipalMetadata) {
  AssertIsOnIOThread();

  uint64_t usage = 0;

  {
    MutexAutoLock lock(mQuotaMutex);

    GroupInfoPair* pair;
    if (mGroupInfoPairs.Get(aPrincipalMetadata.mGroup, &pair)) {
      for (const PersistenceType type : kBestEffortPersistenceTypes) {
        RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(type);
        if (groupInfo) {
          RefPtr<OriginInfo> originInfo =
              groupInfo->LockedGetOriginInfo(aPrincipalMetadata.mOrigin);
          if (originInfo) {
            AssertNoOverflow(usage, originInfo->LockedUsage());
            usage += originInfo->LockedUsage();
          }
        }
      }
    }
  }

  return usage;
}

Maybe<FullOriginMetadata> QuotaManager::GetFullOriginMetadata(
    const OriginMetadata& aOriginMetadata) {
  AssertIsOnIOThread();
  MOZ_DIAGNOSTIC_ASSERT(mStorageConnection);
  MOZ_DIAGNOSTIC_ASSERT(mTemporaryStorageInitialized);

  MutexAutoLock lock(mQuotaMutex);

  RefPtr<OriginInfo> originInfo =
      LockedGetOriginInfo(aOriginMetadata.mPersistenceType, aOriginMetadata);
  if (originInfo) {
    return Some(originInfo->LockedFlattenToFullOriginMetadata());
  }

  return Nothing();
}

void QuotaManager::NotifyStoragePressure(uint64_t aUsage) {
  mQuotaMutex.AssertNotCurrentThreadOwns();

  RefPtr<StoragePressureRunnable> storagePressureRunnable =
      new StoragePressureRunnable(aUsage);

  MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(storagePressureRunnable));
}

// static
void QuotaManager::GetStorageId(PersistenceType aPersistenceType,
                                const nsACString& aOrigin,
                                Client::Type aClientType,
                                nsACString& aDatabaseId) {
  nsAutoCString str;
  str.AppendInt(aPersistenceType);
  str.Append('*');
  str.Append(aOrigin);
  str.Append('*');
  str.AppendInt(aClientType);

  aDatabaseId = str;
}

// static
bool QuotaManager::IsPrincipalInfoValid(const PrincipalInfo& aPrincipalInfo) {
  switch (aPrincipalInfo.type()) {
    // A system principal is acceptable.
    case PrincipalInfo::TSystemPrincipalInfo: {
      return true;
    }

    // Validate content principals to ensure that the spec, originNoSuffix and
    // baseDomain are sane.
    case PrincipalInfo::TContentPrincipalInfo: {
      const ContentPrincipalInfo& info =
          aPrincipalInfo.get_ContentPrincipalInfo();

      // Verify the principal spec parses.
      nsCOMPtr<nsIURI> uri;
      QM_TRY(MOZ_TO_RESULT(NS_NewURI(getter_AddRefs(uri), info.spec())), false);

      nsCOMPtr<nsIPrincipal> principal =
          BasePrincipal::CreateContentPrincipal(uri, info.attrs());
      QM_TRY(MOZ_TO_RESULT(principal), false);

      // Verify the principal originNoSuffix matches spec.
      QM_TRY_INSPECT(const auto& originNoSuffix,
                     MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoCString, principal,
                                                       GetOriginNoSuffix),
                     false);

      if (NS_WARN_IF(originNoSuffix != info.originNoSuffix())) {
        QM_WARNING("originNoSuffix (%s) doesn't match passed one (%s)!",
                   originNoSuffix.get(), info.originNoSuffix().get());
        return false;
      }

      if (NS_WARN_IF(info.originNoSuffix().EqualsLiteral(kChromeOrigin))) {
        return false;
      }

      if (NS_WARN_IF(info.originNoSuffix().FindChar('^', 0) != -1)) {
        QM_WARNING("originNoSuffix (%s) contains the '^' character!",
                   info.originNoSuffix().get());
        return false;
      }

      // Verify the principal baseDomain exists.
      if (NS_WARN_IF(info.baseDomain().IsVoid())) {
        return false;
      }

      // Verify the principal baseDomain matches spec.
      QM_TRY_INSPECT(const auto& baseDomain,
                     MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoCString, principal,
                                                       GetBaseDomain),
                     false);

      if (NS_WARN_IF(baseDomain != info.baseDomain())) {
        QM_WARNING("baseDomain (%s) doesn't match passed one (%s)!",
                   baseDomain.get(), info.baseDomain().get());
        return false;
      }

      return true;
    }

    default: {
      break;
    }
  }

  // Null and expanded principals are not acceptable.
  return false;
}

Result<PrincipalMetadata, nsresult>
QuotaManager::GetInfoFromValidatedPrincipalInfo(
    const PrincipalInfo& aPrincipalInfo) {
  MOZ_ASSERT(IsPrincipalInfoValid(aPrincipalInfo));

  switch (aPrincipalInfo.type()) {
    case PrincipalInfo::TSystemPrincipalInfo: {
      return GetInfoForChrome();
    }

    case PrincipalInfo::TContentPrincipalInfo: {
      const ContentPrincipalInfo& info =
          aPrincipalInfo.get_ContentPrincipalInfo();

      nsCString suffix;
      info.attrs().CreateSuffix(suffix);

      nsCString origin = info.originNoSuffix() + suffix;

      if (StringBeginsWith(origin, kUUIDOriginScheme)) {
        QM_TRY_INSPECT(const auto& originalOrigin,
                       GetOriginFromStorageOrigin(origin));

        nsCOMPtr<nsIPrincipal> principal =
            BasePrincipal::CreateContentPrincipal(originalOrigin);
        QM_TRY(MOZ_TO_RESULT(principal));

        PrincipalInfo principalInfo;
        QM_TRY(
            MOZ_TO_RESULT(PrincipalToPrincipalInfo(principal, &principalInfo)));

        return GetInfoFromValidatedPrincipalInfo(principalInfo);
      }

      PrincipalMetadata principalMetadata;

      principalMetadata.mSuffix = suffix;

      principalMetadata.mGroup = info.baseDomain() + suffix;

      principalMetadata.mOrigin = origin;

      if (info.attrs().mPrivateBrowsingId != 0) {
        QM_TRY_UNWRAP(principalMetadata.mStorageOrigin,
                      EnsureStorageOriginFromOrigin(origin));
      } else {
        principalMetadata.mStorageOrigin = origin;
      }

      principalMetadata.mIsPrivate = info.attrs().mPrivateBrowsingId != 0;

      return principalMetadata;
    }

    default: {
      MOZ_ASSERT_UNREACHABLE("Should never get here!");
      return Err(NS_ERROR_UNEXPECTED);
    }
  }
}

// static
nsAutoCString QuotaManager::GetOriginFromValidatedPrincipalInfo(
    const PrincipalInfo& aPrincipalInfo) {
  MOZ_ASSERT(IsPrincipalInfoValid(aPrincipalInfo));

  switch (aPrincipalInfo.type()) {
    case PrincipalInfo::TSystemPrincipalInfo: {
      return nsAutoCString{GetOriginForChrome()};
    }

    case PrincipalInfo::TContentPrincipalInfo: {
      const ContentPrincipalInfo& info =
          aPrincipalInfo.get_ContentPrincipalInfo();

      nsAutoCString suffix;

      info.attrs().CreateSuffix(suffix);

      return info.originNoSuffix() + suffix;
    }

    default: {
      MOZ_CRASH("Should never get here!");
    }
  }
}

// static
Result<PrincipalMetadata, nsresult> QuotaManager::GetInfoFromPrincipal(
    nsIPrincipal* aPrincipal) {
  MOZ_ASSERT(aPrincipal);

  if (aPrincipal->IsSystemPrincipal()) {
    return GetInfoForChrome();
  }

  if (aPrincipal->GetIsNullPrincipal()) {
    NS_WARNING("IndexedDB not supported from this principal!");
    return Err(NS_ERROR_FAILURE);
  }

  PrincipalMetadata principalMetadata;

  QM_TRY(MOZ_TO_RESULT(aPrincipal->GetOrigin(principalMetadata.mOrigin)));

  if (principalMetadata.mOrigin.EqualsLiteral(kChromeOrigin)) {
    NS_WARNING("Non-chrome principal can't use chrome origin!");
    return Err(NS_ERROR_FAILURE);
  }

  aPrincipal->OriginAttributesRef().CreateSuffix(principalMetadata.mSuffix);

  nsAutoCString baseDomain;
  QM_TRY(MOZ_TO_RESULT(aPrincipal->GetBaseDomain(baseDomain)));

  MOZ_ASSERT(!baseDomain.IsEmpty());

  principalMetadata.mGroup = baseDomain + principalMetadata.mSuffix;

  principalMetadata.mStorageOrigin = principalMetadata.mOrigin;

  principalMetadata.mIsPrivate = aPrincipal->GetPrivateBrowsingId() != 0;

  return principalMetadata;
}

Result<PrincipalMetadata, nsresult> QuotaManager::GetInfoFromWindow(
    nsPIDOMWindowOuter* aWindow) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aWindow);

  nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(aWindow);
  QM_TRY(OkIf(sop), Err(NS_ERROR_FAILURE));

  nsCOMPtr<nsIPrincipal> principal = sop->GetPrincipal();
  QM_TRY(OkIf(principal), Err(NS_ERROR_FAILURE));

  return GetInfoFromPrincipal(principal);
}

// static
Result<nsAutoCString, nsresult> QuotaManager::GetOriginFromPrincipal(
    nsIPrincipal* aPrincipal) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aPrincipal);

  if (aPrincipal->IsSystemPrincipal()) {
    return nsAutoCString{GetOriginForChrome()};
  }

  if (aPrincipal->GetIsNullPrincipal()) {
    NS_WARNING("IndexedDB not supported from this principal!");
    return Err(NS_ERROR_FAILURE);
  }

  QM_TRY_UNWRAP(const auto origin, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
                                       nsAutoCString, aPrincipal, GetOrigin));

  if (origin.EqualsLiteral(kChromeOrigin)) {
    NS_WARNING("Non-chrome principal can't use chrome origin!");
    return Err(NS_ERROR_FAILURE);
  }

  return origin;
}

// static
Result<nsAutoCString, nsresult> QuotaManager::GetOriginFromWindow(
    nsPIDOMWindowOuter* aWindow) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aWindow);

  nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(aWindow);
  QM_TRY(OkIf(sop), Err(NS_ERROR_FAILURE));

  nsCOMPtr<nsIPrincipal> principal = sop->GetPrincipal();
  QM_TRY(OkIf(principal), Err(NS_ERROR_FAILURE));

  QM_TRY_RETURN(GetOriginFromPrincipal(principal));
}

// static
PrincipalMetadata QuotaManager::GetInfoForChrome() {
  return {{},
          GetOriginForChrome(),
          GetOriginForChrome(),
          GetOriginForChrome(),
          false};
}

// static
nsLiteralCString QuotaManager::GetOriginForChrome() {
  return nsLiteralCString{kChromeOrigin};
}

// static
bool QuotaManager::IsOriginInternal(const nsACString& aOrigin) {
  MOZ_ASSERT(!aOrigin.IsEmpty());

  // The first prompt is not required for these origins.
  if (aOrigin.EqualsLiteral(kChromeOrigin) ||
      StringBeginsWith(aOrigin, nsDependentCString(kAboutHomeOriginPrefix)) ||
      StringBeginsWith(aOrigin, nsDependentCString(kIndexedDBOriginPrefix)) ||
      StringBeginsWith(aOrigin, nsDependentCString(kResourceOriginPrefix))) {
    return true;
  }

  return false;
}

// static
bool QuotaManager::AreOriginsEqualOnDisk(const nsACString& aOrigin1,
                                         const nsACString& aOrigin2) {
  return MakeSanitizedOriginCString(aOrigin1) ==
         MakeSanitizedOriginCString(aOrigin2);
}

// static
Result<PrincipalInfo, nsresult> QuotaManager::ParseOrigin(
    const nsACString& aOrigin) {
  // An origin string either corresponds to a SystemPrincipalInfo or a
  // ContentPrincipalInfo, see
  // QuotaManager::GetOriginFromValidatedPrincipalInfo.

  if (aOrigin.Equals(kChromeOrigin)) {
    return PrincipalInfo{SystemPrincipalInfo{}};
  }

  ContentPrincipalInfo contentPrincipalInfo;

  nsCString originalSuffix;
  const OriginParser::ResultType result = OriginParser::ParseOrigin(
      MakeSanitizedOriginCString(aOrigin), contentPrincipalInfo.spec(),
      &contentPrincipalInfo.attrs(), originalSuffix);
  QM_TRY(OkIf(result == OriginParser::ValidOrigin), Err(NS_ERROR_FAILURE));

  return PrincipalInfo{std::move(contentPrincipalInfo)};
}

// static
void QuotaManager::InvalidateQuotaCache() { gInvalidateQuotaCache = true; }

uint64_t QuotaManager::LockedCollectOriginsForEviction(
    uint64_t aMinSizeToBeFreed, nsTArray<RefPtr<OriginDirectoryLock>>& aLocks) {
  mQuotaMutex.AssertCurrentThreadOwns();

  RefPtr<CollectOriginsHelper> helper =
      new CollectOriginsHelper(mQuotaMutex, aMinSizeToBeFreed);

  // Unlock while calling out to XPCOM (code behind the dispatch method needs
  // to acquire its own lock which can potentially lead to a deadlock and it
  // also calls an observer that can do various stuff like IO, so it's better
  // to not hold our mutex while that happens).
  {
    MutexAutoUnlock autoUnlock(mQuotaMutex);

    MOZ_ALWAYS_SUCCEEDS(mOwningThread->Dispatch(helper, NS_DISPATCH_NORMAL));
  }

  return helper->BlockAndReturnOriginsForEviction(aLocks);
}

void QuotaManager::LockedRemoveQuotaForRepository(
    PersistenceType aPersistenceType) {
  mQuotaMutex.AssertCurrentThreadOwns();
  MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT);

  for (auto iter = mGroupInfoPairs.Iter(); !iter.Done(); iter.Next()) {
    auto& pair = iter.Data();

    if (RefPtr<GroupInfo> groupInfo =
            pair->LockedGetGroupInfo(aPersistenceType)) {
      groupInfo->LockedRemoveOriginInfos();

      pair->LockedClearGroupInfo(aPersistenceType);

      if (!pair->LockedHasGroupInfos()) {
        iter.Remove();
      }
    }
  }
}

void QuotaManager::LockedRemoveQuotaForOrigin(
    const OriginMetadata& aOriginMetadata) {
  mQuotaMutex.AssertCurrentThreadOwns();
  MOZ_ASSERT(aOriginMetadata.mPersistenceType != PERSISTENCE_TYPE_PERSISTENT);

  GroupInfoPair* pair;
  if (!mGroupInfoPairs.Get(aOriginMetadata.mGroup, &pair)) {
    return;
  }

  MOZ_ASSERT(pair);

  if (RefPtr<GroupInfo> groupInfo =
          pair->LockedGetGroupInfo(aOriginMetadata.mPersistenceType)) {
    groupInfo->LockedRemoveOriginInfo(aOriginMetadata.mOrigin);

    if (!groupInfo->LockedHasOriginInfos()) {
      pair->LockedClearGroupInfo(aOriginMetadata.mPersistenceType);

      if (!pair->LockedHasGroupInfos()) {
        mGroupInfoPairs.Remove(aOriginMetadata.mGroup);
      }
    }
  }
}

already_AddRefed<GroupInfo> QuotaManager::LockedGetOrCreateGroupInfo(
    PersistenceType aPersistenceType, const nsACString& aSuffix,
    const nsACString& aGroup) {
  mQuotaMutex.AssertCurrentThreadOwns();
  MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT);

  GroupInfoPair* const pair =
      mGroupInfoPairs.GetOrInsertNew(aGroup, aSuffix, aGroup);

  RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(aPersistenceType);
  if (!groupInfo) {
    groupInfo = new GroupInfo(pair, aPersistenceType);
    pair->LockedSetGroupInfo(aPersistenceType, groupInfo);
  }

  return groupInfo.forget();
}

already_AddRefed<OriginInfo> QuotaManager::LockedGetOriginInfo(
    PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata) {
  mQuotaMutex.AssertCurrentThreadOwns();
  MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT);

  GroupInfoPair* pair;
  if (mGroupInfoPairs.Get(aOriginMetadata.mGroup, &pair)) {
    RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(aPersistenceType);
    if (groupInfo) {
      return groupInfo->LockedGetOriginInfo(aOriginMetadata.mOrigin);
    }
  }

  return nullptr;
}

template <typename Iterator>
void QuotaManager::MaybeInsertNonPersistedOriginInfos(
    Iterator aDest, const RefPtr<GroupInfo>& aTemporaryGroupInfo,
    const RefPtr<GroupInfo>& aDefaultGroupInfo,
    const RefPtr<GroupInfo>& aPrivateGroupInfo) {
  const auto copy = [&aDest](const GroupInfo& groupInfo) {
    std::copy_if(
        groupInfo.mOriginInfos.cbegin(), groupInfo.mOriginInfos.cend(), aDest,
        [](const auto& originInfo) { return !originInfo->LockedPersisted(); });
  };

  if (aTemporaryGroupInfo) {
    MOZ_ASSERT(PERSISTENCE_TYPE_TEMPORARY ==
               aTemporaryGroupInfo->GetPersistenceType());

    copy(*aTemporaryGroupInfo);
  }
  if (aDefaultGroupInfo) {
    MOZ_ASSERT(PERSISTENCE_TYPE_DEFAULT ==
               aDefaultGroupInfo->GetPersistenceType());

    copy(*aDefaultGroupInfo);
  }
  if (aPrivateGroupInfo) {
    MOZ_ASSERT(PERSISTENCE_TYPE_PRIVATE ==
               aPrivateGroupInfo->GetPersistenceType());
    copy(*aPrivateGroupInfo);
  }
}

template <typename Collect, typename Pred>
QuotaManager::OriginInfosFlatTraversable
QuotaManager::CollectLRUOriginInfosUntil(Collect&& aCollect, Pred&& aPred) {
  OriginInfosFlatTraversable originInfos;

  std::forward<Collect>(aCollect)(MakeBackInserter(originInfos));

  originInfos.Sort(OriginInfoAccessTimeComparator());

  const auto foundIt = std::find_if(originInfos.cbegin(), originInfos.cend(),
                                    std::forward<Pred>(aPred));

  originInfos.TruncateLength(foundIt - originInfos.cbegin());

  return originInfos;
}

QuotaManager::OriginInfosNestedTraversable
QuotaManager::GetOriginInfosExceedingGroupLimit() const {
  MutexAutoLock lock(mQuotaMutex);

  OriginInfosNestedTraversable originInfos;

  for (const auto& entry : mGroupInfoPairs) {
    const auto& pair = entry.GetData();

    MOZ_ASSERT(!entry.GetKey().IsEmpty());
    MOZ_ASSERT(pair);

    uint64_t groupUsage = 0;

    const RefPtr<GroupInfo> temporaryGroupInfo =
        pair->LockedGetGroupInfo(PERSISTENCE_TYPE_TEMPORARY);
    if (temporaryGroupInfo) {
      groupUsage += temporaryGroupInfo->mUsage;
    }

    const RefPtr<GroupInfo> defaultGroupInfo =
        pair->LockedGetGroupInfo(PERSISTENCE_TYPE_DEFAULT);
    if (defaultGroupInfo) {
      groupUsage += defaultGroupInfo->mUsage;
    }

    const RefPtr<GroupInfo> privateGroupInfo =
        pair->LockedGetGroupInfo(PERSISTENCE_TYPE_PRIVATE);
    if (privateGroupInfo) {
      groupUsage += privateGroupInfo->mUsage;
    }

    if (groupUsage > 0) {
      QuotaManager* quotaManager = QuotaManager::Get();
      MOZ_ASSERT(quotaManager, "Shouldn't be null!");

      if (groupUsage > quotaManager->GetGroupLimit()) {
        originInfos.AppendElement(CollectLRUOriginInfosUntil(
            [&temporaryGroupInfo, &defaultGroupInfo,
             &privateGroupInfo](auto inserter) {
              MaybeInsertNonPersistedOriginInfos(
                  std::move(inserter), temporaryGroupInfo, defaultGroupInfo,
                  privateGroupInfo);
            },
            [&groupUsage, quotaManager](const auto& originInfo) {
              groupUsage -= originInfo->LockedUsage();

              return groupUsage <= quotaManager->GetGroupLimit();
            }));
      }
    }
  }

  return originInfos;
}

QuotaManager::OriginInfosNestedTraversable
QuotaManager::GetOriginInfosExceedingGlobalLimit() const {
  MutexAutoLock lock(mQuotaMutex);

  QuotaManager::OriginInfosNestedTraversable res;
  res.AppendElement(CollectLRUOriginInfosUntil(
      // XXX The lambda only needs to capture this, but due to Bug 1421435 it
      // can't.
      [&](auto inserter) {
        for (const auto& entry : mGroupInfoPairs) {
          const auto& pair = entry.GetData();

          MOZ_ASSERT(!entry.GetKey().IsEmpty());
          MOZ_ASSERT(pair);

          MaybeInsertNonPersistedOriginInfos(
              inserter, pair->LockedGetGroupInfo(PERSISTENCE_TYPE_TEMPORARY),
              pair->LockedGetGroupInfo(PERSISTENCE_TYPE_DEFAULT),
              pair->LockedGetGroupInfo(PERSISTENCE_TYPE_PRIVATE));
        }
      },
      [temporaryStorageUsage = mTemporaryStorageUsage,
       temporaryStorageLimit = mTemporaryStorageLimit,
       doomedUsage = uint64_t{0}](const auto& originInfo) mutable {
        if (temporaryStorageUsage - doomedUsage <= temporaryStorageLimit) {
          return true;
        }

        doomedUsage += originInfo->LockedUsage();
        return false;
      }));

  return res;
}

void QuotaManager::ClearOrigins(
    const OriginInfosNestedTraversable& aDoomedOriginInfos) {
  AssertIsOnIOThread();

  // If we are in shutdown, we could break off early from clearing origins.
  // In such cases, we would like to track the ones that were already cleared
  // up, such that other essential cleanup could be performed on clearedOrigins.
  // clearedOrigins is used in calls to LockedRemoveQuotaForOrigin and
  // OriginClearCompleted below. We could have used a collection of OriginInfos
  // rather than flattening them to OriginMetadata but groupInfo in OriginInfo
  // is just a raw ptr and LockedRemoveQuotaForOrigin might delete groupInfo and
  // as a result, we would not be able to get origin persistence type required
  // in OriginClearCompleted call after lockedRemoveQuotaForOrigin call.
  nsTArray<OriginMetadata> clearedOrigins;

  // XXX Does this need to be done a) in order and/or b) sequentially?
  for (const auto& doomedOriginInfo :
       Flatten<OriginInfosFlatTraversable::value_type>(aDoomedOriginInfos)) {
#ifdef DEBUG
    {
      MutexAutoLock lock(mQuotaMutex);
      MOZ_ASSERT(!doomedOriginInfo->LockedPersisted());
    }
#endif

    //  TODO: We are currently only checking for this flag here which
    //  means that we cannot break off once we start cleaning an origin. It
    //  could be better if we could check for shutdown flag while cleaning an
    //  origin such that we could break off early from the cleaning process if
    //  we are stuck cleaning on one huge origin. Bug1797098 has been filed to
    //  track this.
    if (QuotaManager::IsShuttingDown()) {
      break;
    }

    auto originMetadata = doomedOriginInfo->FlattenToOriginMetadata();

    DeleteOriginDirectory(originMetadata);

    clearedOrigins.AppendElement(std::move(originMetadata));
  }

  {
    MutexAutoLock lock(mQuotaMutex);

    for (const auto& clearedOrigin : clearedOrigins) {
      LockedRemoveQuotaForOrigin(clearedOrigin);
    }
  }

  for (const auto& clearedOrigin : clearedOrigins) {
    OriginClearCompleted(clearedOrigin.mPersistenceType, clearedOrigin.mOrigin,
                         Nullable<Client::Type>());
  }
}

void QuotaManager::CleanupTemporaryStorage() {
  AssertIsOnIOThread();

  // Evicting origins that exceed their group limit also affects the global
  // temporary storage usage, so these steps have to be taken sequentially.
  // Combining them doesn't seem worth the added complexity.
  ClearOrigins(GetOriginInfosExceedingGroupLimit());
  ClearOrigins(GetOriginInfosExceedingGlobalLimit());

  if (mTemporaryStorageUsage > mTemporaryStorageLimit) {
    // If disk space is still low after origin clear, notify storage pressure.
    NotifyStoragePressure(mTemporaryStorageUsage);
  }
}

void QuotaManager::DeleteOriginDirectory(
    const OriginMetadata& aOriginMetadata) {
  QM_TRY_INSPECT(const auto& directory, GetOriginDirectory(aOriginMetadata),
                 QM_VOID);

  nsresult rv = directory->Remove(true);
  if (rv != NS_ERROR_FILE_NOT_FOUND && NS_FAILED(rv)) {
    // This should never fail if we've closed all storage connections
    // correctly...
    NS_ERROR("Failed to remove directory!");
  }
}

void QuotaManager::FinalizeOriginEviction(
    nsTArray<RefPtr<OriginDirectoryLock>>&& aLocks) {
  NS_ASSERTION(!NS_IsMainThread(), "Wrong thread!");

  RefPtr<OriginOperationBase> op =
      CreateFinalizeOriginEvictionOp(mOwningThread, std::move(aLocks));

  if (IsOnBackgroundThread()) {
    op->RunImmediately();
  } else {
    op->Dispatch();
  }
}

Result<Ok, nsresult> QuotaManager::ArchiveOrigins(
    const nsTArray<FullOriginMetadata>& aFullOriginMetadatas) {
  AssertIsOnIOThread();
  MOZ_ASSERT(!aFullOriginMetadatas.IsEmpty());

  QM_TRY_INSPECT(const auto& storageArchivesDir,
                 QM_NewLocalFile(*mStorageArchivesPath));

  // Create another subdir, so once we decide to remove all temporary archives,
  // we can remove only the subdir and the parent directory can still be used
  // for something else or similar in future. Otherwise, we would have to
  // figure out a new name for it.
  QM_TRY(MOZ_TO_RESULT(storageArchivesDir->Append(u"0"_ns)));

  PRExplodedTime now;
  PR_ExplodeTime(PR_Now(), PR_LocalTimeParameters, &now);

  const auto dateStr =
      nsPrintfCString("%04hd-%02" PRId32 "-%02" PRId32, now.tm_year,
                      now.tm_month + 1, now.tm_mday);

  QM_TRY_INSPECT(
      const auto& storageArchiveDir,
      CloneFileAndAppend(*storageArchivesDir, NS_ConvertASCIItoUTF16(dateStr)));

  QM_TRY(MOZ_TO_RESULT(
      storageArchiveDir->CreateUnique(nsIFile::DIRECTORY_TYPE, 0700)));

  QM_TRY_INSPECT(const auto& defaultStorageArchiveDir,
                 CloneFileAndAppend(*storageArchiveDir,
                                    nsLiteralString(DEFAULT_DIRECTORY_NAME)));

  QM_TRY_INSPECT(const auto& temporaryStorageArchiveDir,
                 CloneFileAndAppend(*storageArchiveDir,
                                    nsLiteralString(TEMPORARY_DIRECTORY_NAME)));

  for (const auto& fullOriginMetadata : aFullOriginMetadatas) {
    MOZ_ASSERT(
        IsBestEffortPersistenceType(fullOriginMetadata.mPersistenceType));

    QM_TRY_INSPECT(const auto& directory,
                   GetOriginDirectory(fullOriginMetadata));

    // The origin could have been removed, for example due to corruption.
    QM_TRY_INSPECT(
        const auto& moved,
        QM_OR_ELSE_WARN_IF(
            // Expression.
            MOZ_TO_RESULT(
                directory->MoveTo(fullOriginMetadata.mPersistenceType ==
                                          PERSISTENCE_TYPE_DEFAULT
                                      ? defaultStorageArchiveDir
                                      : temporaryStorageArchiveDir,
                                  u""_ns))
                .map([](Ok) { return true; }),
            // Predicate.
            ([](const nsresult rv) { return rv == NS_ERROR_FILE_NOT_FOUND; }),
            // Fallback.
            ErrToOk<false>));

    if (moved) {
      RemoveQuotaForOrigin(fullOriginMetadata.mPersistenceType,
                           fullOriginMetadata);
    }
  }

  return Ok{};
}

auto QuotaManager::GetDirectoryLockTable(PersistenceType aPersistenceType)
    -> DirectoryLockTable& {
  switch (aPersistenceType) {
    case PERSISTENCE_TYPE_TEMPORARY:
      return mTemporaryDirectoryLockTable;
    case PERSISTENCE_TYPE_DEFAULT:
      return mDefaultDirectoryLockTable;
    case PERSISTENCE_TYPE_PRIVATE:
      return mPrivateDirectoryLockTable;

    case PERSISTENCE_TYPE_PERSISTENT:
    case PERSISTENCE_TYPE_INVALID:
    default:
      MOZ_CRASH("Bad persistence type value!");
  }
}

bool QuotaManager::IsSanitizedOriginValid(const nsACString& aSanitizedOrigin) {
  AssertIsOnIOThread();

  // Do not parse this sanitized origin string, if we already parsed it.
  return mValidOrigins.LookupOrInsertWith(
      aSanitizedOrigin, [&aSanitizedOrigin] {
        nsCString spec;
        OriginAttributes attrs;
        nsCString originalSuffix;
        const auto result = OriginParser::ParseOrigin(aSanitizedOrigin, spec,
                                                      &attrs, originalSuffix);

        return result == OriginParser::ValidOrigin;
      });
}

Result<nsCString, nsresult> QuotaManager::EnsureStorageOriginFromOrigin(
    const nsACString& aOrigin) {
  MutexAutoLock lock(mQuotaMutex);

  QM_TRY_UNWRAP(
      auto storageOrigin,
      mOriginToStorageOriginMap.TryLookupOrInsertWith(
          aOrigin, [this, &aOrigin]() -> Result<nsCString, nsresult> {
            OriginAttributes originAttributes;

            nsCString originNoSuffix;
            QM_TRY(MOZ_TO_RESULT(
                originAttributes.PopulateFromOrigin(aOrigin, originNoSuffix)));

            nsCOMPtr<nsIURI> uri;
            QM_TRY(MOZ_TO_RESULT(
                NS_MutateURI(NS_STANDARDURLMUTATOR_CONTRACTID)
                    .SetSpec(originNoSuffix)
                    .SetScheme(kUUIDOriginScheme)
                    .SetHost(NSID_TrimBracketsASCII(nsID::GenerateUUID()))
                    .SetPort(-1)
                    .Finalize(uri)));

            nsCOMPtr<nsIPrincipal> principal =
                BasePrincipal::CreateContentPrincipal(uri, OriginAttributes{});
            QM_TRY(MOZ_TO_RESULT(principal));

            QM_TRY_UNWRAP(auto origin,
                          MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
                              nsAutoCString, principal, GetOrigin));

            mStorageOriginToOriginMap.WithEntryHandle(
                origin,
                [&aOrigin](auto entryHandle) { entryHandle.Insert(aOrigin); });

            return nsCString(std::move(origin));
          }));

  return nsCString(std::move(storageOrigin));
}

Result<nsCString, nsresult> QuotaManager::GetOriginFromStorageOrigin(
    const nsACString& aStorageOrigin) {
  MutexAutoLock lock(mQuotaMutex);

  auto maybeOrigin = mStorageOriginToOriginMap.MaybeGet(aStorageOrigin);
  if (maybeOrigin.isNothing()) {
    return Err(NS_ERROR_FAILURE);
  }

  return maybeOrigin.ref();
}

int64_t QuotaManager::GenerateDirectoryLockId() {
  const int64_t directorylockId = mNextDirectoryLockId;

  if (CheckedInt64 result = CheckedInt64(mNextDirectoryLockId) + 1;
      result.isValid()) {
    mNextDirectoryLockId = result.value();
  } else {
    NS_WARNING("Quota manager has run out of ids for directory locks!");

    // There's very little chance for this to happen given the max size of
    // 64 bit integer but if it happens we can just reset mNextDirectoryLockId
    // to zero since such old directory locks shouldn't exist anymore.
    mNextDirectoryLockId = 0;
  }

  // TODO: Maybe add an assertion here to check that there is no existing
  //       directory lock with given id.

  return directorylockId;
}

template <typename Func>
auto QuotaManager::ExecuteInitialization(const Initialization aInitialization,
                                         Func&& aFunc)
    -> std::invoke_result_t<Func, const FirstInitializationAttempt<
                                      Initialization, StringGenerator>&> {
  return quota::ExecuteInitialization(mInitializationInfo, aInitialization,
                                      std::forward<Func>(aFunc));
}

template <typename Func>
auto QuotaManager::ExecuteInitialization(const Initialization aInitialization,
                                         const nsACString& aContext,
                                         Func&& aFunc)
    -> std::invoke_result_t<Func, const FirstInitializationAttempt<
                                      Initialization, StringGenerator>&> {
  return quota::ExecuteInitialization(mInitializationInfo, aInitialization,
                                      aContext, std::forward<Func>(aFunc));
}

template <typename Func>
auto QuotaManager::ExecuteOriginInitialization(
    const nsACString& aOrigin, const OriginInitialization aInitialization,
    const nsACString& aContext, Func&& aFunc)
    -> std::invoke_result_t<Func, const FirstInitializationAttempt<
                                      Initialization, StringGenerator>&> {
  return quota::ExecuteInitialization(
      mInitializationInfo.MutableOriginInitializationInfoRef(
          aOrigin, CreateIfNonExistent{}),
      aInitialization, aContext, std::forward<Func>(aFunc));
}

/*******************************************************************************
 * Local class implementations
 ******************************************************************************/

CollectOriginsHelper::CollectOriginsHelper(mozilla::Mutex& aMutex,
                                           uint64_t aMinSizeToBeFreed)
    : Runnable("dom::quota::CollectOriginsHelper"),
      mMinSizeToBeFreed(aMinSizeToBeFreed),
      mMutex(aMutex),
      mCondVar(aMutex, "CollectOriginsHelper::mCondVar"),
      mSizeToBeFreed(0),
      mWaiting(true) {
  MOZ_ASSERT(!NS_IsMainThread(), "Wrong thread!");
  mMutex.AssertCurrentThreadOwns();
}

int64_t CollectOriginsHelper::BlockAndReturnOriginsForEviction(
    nsTArray<RefPtr<OriginDirectoryLock>>& aLocks) {
  MOZ_ASSERT(!NS_IsMainThread(), "Wrong thread!");
  mMutex.AssertCurrentThreadOwns();

  while (mWaiting) {
    mCondVar.Wait();
  }

  mLocks.SwapElements(aLocks);
  return mSizeToBeFreed;
}

NS_IMETHODIMP
CollectOriginsHelper::Run() {
  AssertIsOnBackgroundThread();

  QuotaManager* quotaManager = QuotaManager::Get();
  NS_ASSERTION(quotaManager, "Shouldn't be null!");

  // We use extra stack vars here to avoid race detector warnings (the same
  // memory accessed with and without the lock held).
  nsTArray<RefPtr<OriginDirectoryLock>> locks;
  uint64_t sizeToBeFreed =
      quotaManager->CollectOriginsForEviction(mMinSizeToBeFreed, locks);

  MutexAutoLock lock(mMutex);

  NS_ASSERTION(mWaiting, "Huh?!");

  mLocks.SwapElements(locks);
  mSizeToBeFreed = sizeToBeFreed;
  mWaiting = false;
  mCondVar.Notify();

  return NS_OK;
}

NS_IMETHODIMP
StoragePressureRunnable::Run() {
  MOZ_ASSERT(NS_IsMainThread());

  nsCOMPtr<nsIObserverService> obsSvc = mozilla::services::GetObserverService();
  if (NS_WARN_IF(!obsSvc)) {
    return NS_ERROR_FAILURE;
  }

  nsCOMPtr<nsISupportsPRUint64> wrapper =
      do_CreateInstance(NS_SUPPORTS_PRUINT64_CONTRACTID);
  if (NS_WARN_IF(!wrapper)) {
    return NS_ERROR_FAILURE;
  }

  wrapper->SetData(mUsage);

  obsSvc->NotifyObservers(wrapper, "QuotaManager::StoragePressure", u"");

  return NS_OK;
}

TimeStamp RecordQuotaInfoLoadTimeHelper::Start() {
  AssertIsOnIOThread();

  // XXX: If a OS sleep/wake occur after mStartTime is initialized but before
  // gLastOSWake is set, then this time duration would still be recorded with
  // key "Normal". We are assumming this is rather rare to happen.
  mStartTime.init(TimeStamp::Now());
  MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(this));

  return *mStartTime;
}

TimeStamp RecordQuotaInfoLoadTimeHelper::End() {
  AssertIsOnIOThread();

  mEndTime.init(TimeStamp::Now());
  MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(this));

  return *mEndTime;
}

NS_IMETHODIMP
RecordQuotaInfoLoadTimeHelper::Run() {
  MOZ_ASSERT(NS_IsMainThread());

  if (mInitializedTime.isSome()) {
    // Keys for QM_QUOTA_INFO_LOAD_TIME_V0:
    // Normal: Normal conditions.
    // WasSuspended: There was a OS sleep so that it was suspended.
    // TimeStampErr1: The recorded start time is unexpectedly greater than the
    //                end time.
    // TimeStampErr2: The initialized time for the recording class is unexpectly
    //                greater than the last OS wake time.
    const auto key = [this, wasSuspended = gLastOSWake > *mInitializedTime]() {
      if (wasSuspended) {
        return "WasSuspended"_ns;
      }

      // XXX File a bug if we have data for this key.
      // We found negative values in our query in STMO for
      // ScalarID::QM_REPOSITORIES_INITIALIZATION_TIME. This shouldn't happen
      // because the documentation for TimeStamp::Now() says it returns a
      // monotonically increasing number.
      if (*mStartTime > *mEndTime) {
        return "TimeStampErr1"_ns;
      }

      if (*mInitializedTime > gLastOSWake) {
        return "TimeStampErr2"_ns;
      }

      return "Normal"_ns;
    }();

    Telemetry::AccumulateTimeDelta(Telemetry::QM_QUOTA_INFO_LOAD_TIME_V0, key,
                                   *mStartTime, *mEndTime);

    return NS_OK;
  }

  gLastOSWake = TimeStamp::Now();
  mInitializedTime.init(gLastOSWake);

  return NS_OK;
}

nsresult StorageOperationBase::GetDirectoryMetadata(nsIFile* aDirectory,
                                                    int64_t& aTimestamp,
                                                    nsACString& aGroup,
                                                    nsACString& aOrigin,
                                                    Nullable<bool>& aIsApp) {
  AssertIsOnIOThread();
  MOZ_ASSERT(aDirectory);

  QM_TRY_INSPECT(
      const auto& binaryStream,
      GetBinaryInputStream(*aDirectory, nsLiteralString(METADATA_FILE_NAME)));

  QM_TRY_INSPECT(const uint64_t& timestamp,
                 MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, Read64));

  QM_TRY_INSPECT(const auto& group, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
                                        nsCString, binaryStream, ReadCString));

  QM_TRY_INSPECT(const auto& origin, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
                                         nsCString, binaryStream, ReadCString));

  Nullable<bool> isApp;
  bool value;
  if (NS_SUCCEEDED(binaryStream->ReadBoolean(&value))) {
    isApp.SetValue(value);
  }

  aTimestamp = timestamp;
  aGroup = group;
  aOrigin = origin;
  aIsApp = std::move(isApp);
  return NS_OK;
}

nsresult StorageOperationBase::GetDirectoryMetadata2(
    nsIFile* aDirectory, int64_t& aTimestamp, nsACString& aSuffix,
    nsACString& aGroup, nsACString& aOrigin, bool& aIsApp) {
  AssertIsOnIOThread();
  MOZ_ASSERT(aDirectory);

  QM_TRY_INSPECT(const auto& binaryStream,
                 GetBinaryInputStream(*aDirectory,
                                      nsLiteralString(METADATA_V2_FILE_NAME)));

  QM_TRY_INSPECT(const uint64_t& timestamp,
                 MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, Read64));

  QM_TRY_INSPECT(const bool& persisted,
                 MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, ReadBoolean));
  Unused << persisted;

  QM_TRY_INSPECT(const bool& reservedData1,
                 MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, Read32));
  Unused << reservedData1;

  QM_TRY_INSPECT(const bool& reservedData2,
                 MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, Read32));
  Unused << reservedData2;

  QM_TRY_INSPECT(const auto& suffix, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
                                         nsCString, binaryStream, ReadCString));

  QM_TRY_INSPECT(const auto& group, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
                                        nsCString, binaryStream, ReadCString));

  QM_TRY_INSPECT(const auto& origin, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
                                         nsCString, binaryStream, ReadCString));

  QM_TRY_INSPECT(const bool& isApp,
                 MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, ReadBoolean));

  aTimestamp = timestamp;
  aSuffix = suffix;
  aGroup = group;
  aOrigin = origin;
  aIsApp = isApp;
  return NS_OK;
}

int64_t StorageOperationBase::GetOriginLastModifiedTime(
    const OriginProps& aOriginProps) {
  return GetLastModifiedTime(*aOriginProps.mPersistenceType,
                             *aOriginProps.mDirectory);
}

nsresult StorageOperationBase::RemoveObsoleteOrigin(
    const OriginProps& aOriginProps) {
  AssertIsOnIOThread();

  QM_WARNING(
      "Deleting obsolete %s directory that is no longer a legal "
      "origin!",
      NS_ConvertUTF16toUTF8(aOriginProps.mLeafName).get());

  QM_TRY(MOZ_TO_RESULT(aOriginProps.mDirectory->Remove(/* recursive */ true)));

  return NS_OK;
}

Result<bool, nsresult> StorageOperationBase::MaybeRenameOrigin(
    const OriginProps& aOriginProps) {
  AssertIsOnIOThread();

  const nsAString& oldLeafName = aOriginProps.mLeafName;

  const auto newLeafName =
      MakeSanitizedOriginString(aOriginProps.mOriginMetadata.mOrigin);

  if (oldLeafName == newLeafName) {
    return false;
  }

  QM_TRY(MOZ_TO_RESULT(QuotaManager::CreateDirectoryMetadata(
      *aOriginProps.mDirectory, aOriginProps.mTimestamp,
      aOriginProps.mOriginMetadata)));

  QM_TRY(MOZ_TO_RESULT(QuotaManager::CreateDirectoryMetadata2(
      *aOriginProps.mDirectory, aOriginProps.mTimestamp,
      /* aPersisted */ false, aOriginProps.mOriginMetadata)));

  QM_TRY_INSPECT(const auto& newFile,
                 MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
                     nsCOMPtr<nsIFile>, *aOriginProps.mDirectory, GetParent));

  QM_TRY(MOZ_TO_RESULT(newFile->Append(newLeafName)));

  QM_TRY_INSPECT(const bool& exists,
                 MOZ_TO_RESULT_INVOKE_MEMBER(newFile, Exists));

  if (exists) {
    QM_WARNING(
        "Can't rename %s directory to %s, the target already exists, removing "
        "instead of renaming!",
        NS_ConvertUTF16toUTF8(oldLeafName).get(),
        NS_ConvertUTF16toUTF8(newLeafName).get());
  }

  QM_TRY(CallWithDelayedRetriesIfAccessDenied(
      [&exists, &aOriginProps, &newLeafName] {
        if (exists) {
          QM_TRY_RETURN(MOZ_TO_RESULT(
              aOriginProps.mDirectory->Remove(/* recursive */ true)));
        }
        QM_TRY_RETURN(MOZ_TO_RESULT(
            aOriginProps.mDirectory->RenameTo(nullptr, newLeafName)));
      },
      StaticPrefs::dom_quotaManager_directoryRemovalOrRenaming_maxRetries(),
      StaticPrefs::dom_quotaManager_directoryRemovalOrRenaming_delayMs()));

  return true;
}

nsresult StorageOperationBase::ProcessOriginDirectories() {
  AssertIsOnIOThread();
  MOZ_ASSERT(!mOriginProps.IsEmpty());

  QuotaManager* quotaManager = QuotaManager::Get();
  MOZ_ASSERT(quotaManager);

  for (auto& originProps : mOriginProps) {
    switch (originProps.mType) {
      case OriginProps::eChrome: {
        originProps.mOriginMetadata = {QuotaManager::GetInfoForChrome(),
                                       *originProps.mPersistenceType};
        break;
      }

      case OriginProps::eContent: {
        nsCOMPtr<nsIURI> uri;
        QM_TRY(
            MOZ_TO_RESULT(NS_NewURI(getter_AddRefs(uri), originProps.mSpec)));

        nsCOMPtr<nsIPrincipal> principal =
            BasePrincipal::CreateContentPrincipal(uri, originProps.mAttrs);
        QM_TRY(MOZ_TO_RESULT(principal));

        PrincipalInfo principalInfo;
        QM_TRY(
            MOZ_TO_RESULT(PrincipalToPrincipalInfo(principal, &principalInfo)));

        QM_WARNONLY_TRY_UNWRAP(
            auto valid,
            MOZ_TO_RESULT(quotaManager->IsPrincipalInfoValid(principalInfo)));

        if (!valid) {
          // Unknown directories during upgrade are allowed. Just warn if we
          // find them.
          UNKNOWN_FILE_WARNING(originProps.mLeafName);
          originProps.mIgnore = true;
          break;
        }

        QM_TRY_UNWRAP(
            auto principalMetadata,
            quotaManager->GetInfoFromValidatedPrincipalInfo(principalInfo));

        originProps.mOriginMetadata = {std::move(principalMetadata),
                                       *originProps.mPersistenceType};

        break;
      }

      case OriginProps::eObsolete: {
        // There's no way to get info for obsolete origins.
        break;
      }

      default:
        MOZ_CRASH("Bad type!");
    }
  }

  // Don't try to upgrade obsolete origins, remove them right after we detect
  // them.
  for (const auto& originProps : mOriginProps) {
    if (originProps.mType == OriginProps::eObsolete) {
      MOZ_ASSERT(originProps.mOriginMetadata.mSuffix.IsEmpty());
      MOZ_ASSERT(originProps.mOriginMetadata.mGroup.IsEmpty());
      MOZ_ASSERT(originProps.mOriginMetadata.mOrigin.IsEmpty());

      QM_TRY(MOZ_TO_RESULT(RemoveObsoleteOrigin(originProps)));
    } else if (!originProps.mIgnore) {
      MOZ_ASSERT(!originProps.mOriginMetadata.mGroup.IsEmpty());
      MOZ_ASSERT(!originProps.mOriginMetadata.mOrigin.IsEmpty());

      QM_TRY(MOZ_TO_RESULT(ProcessOriginDirectory(originProps)));
    }
  }

  return NS_OK;
}

// XXX Do the fallible initialization in a separate non-static member function
// of StorageOperationBase and eventually get rid of this method and use a
// normal constructor instead.
template <typename PersistenceTypeFunc>
nsresult StorageOperationBase::OriginProps::Init(
    PersistenceTypeFunc&& aPersistenceTypeFunc) {
  AssertIsOnIOThread();

  QM_TRY_INSPECT(const auto& leafName,
                 MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoString, *mDirectory,
                                                   GetLeafName));

  nsCString spec;
  OriginAttributes attrs;
  nsCString originalSuffix;
  OriginParser::ResultType result = OriginParser::ParseOrigin(
      NS_ConvertUTF16toUTF8(leafName), spec, &attrs, originalSuffix);
  if (NS_WARN_IF(result == OriginParser::InvalidOrigin)) {
    mType = OriginProps::eInvalid;
    return NS_OK;
  }

  const auto persistenceType = [&]() -> PersistenceType {
    // XXX We shouldn't continue with initialization if OriginParser returned
    // anything else but ValidOrigin. Otherwise, we have to deal with empty
    // spec when the origin is obsolete, like here. The caller should handle
    // the errors. Until it's fixed, we have to treat obsolete origins as
    // origins with unknown/invalid persistence type.
    if (result != OriginParser::ValidOrigin) {
      return PERSISTENCE_TYPE_INVALID;
    }
    return std::forward<PersistenceTypeFunc>(aPersistenceTypeFunc)(spec);
  }();

  mLeafName = leafName;
  mSpec = spec;
  mAttrs = attrs;
  mOriginalSuffix = originalSuffix;
  mPersistenceType.init(persistenceType);
  if (result == OriginParser::ObsoleteOrigin) {
    mType = eObsolete;
  } else if (mSpec.EqualsLiteral(kChromeOrigin)) {
    mType = eChrome;
  } else {
    mType = eContent;
  }

  return NS_OK;
}

// static
auto OriginParser::ParseOrigin(const nsACString& aOrigin, nsCString& aSpec,
                               OriginAttributes* aAttrs,
                               nsCString& aOriginalSuffix) -> ResultType {
  MOZ_ASSERT(!aOrigin.IsEmpty());
  MOZ_ASSERT(aAttrs);

  nsCString origin(aOrigin);
  int32_t pos = origin.RFindChar('^');

  if (pos == kNotFound) {
    aOriginalSuffix.Truncate();
  } else {
    aOriginalSuffix = Substring(origin, pos);
  }

  OriginAttributes originAttributes;

  nsCString originNoSuffix;
  bool ok = originAttributes.PopulateFromOrigin(aOrigin, originNoSuffix);
  if (!ok) {
    return InvalidOrigin;
  }

  OriginParser parser(originNoSuffix);

  *aAttrs = originAttributes;
  return parser.Parse(aSpec);
}

auto OriginParser::Parse(nsACString& aSpec) -> ResultType {
  while (mTokenizer.hasMoreTokens()) {
    const nsDependentCSubstring& token = mTokenizer.nextToken();

    HandleToken(token);

    if (mError) {
      break;
    }

    if (!mHandledTokens.IsEmpty()) {
      mHandledTokens.AppendLiteral(", ");
    }
    mHandledTokens.Append('\'');
    mHandledTokens.Append(token);
    mHandledTokens.Append('\'');
  }

  if (!mError && mTokenizer.separatorAfterCurrentToken()) {
    HandleTrailingSeparator();
  }

  if (mError) {
    QM_WARNING("Origin '%s' failed to parse, handled tokens: %s", mOrigin.get(),
               mHandledTokens.get());

    return (mSchemeType == eChrome || mSchemeType == eAbout) ? ObsoleteOrigin
                                                             : InvalidOrigin;
  }

  MOZ_ASSERT(mState == eComplete || mState == eHandledTrailingSeparator);

  // For IPv6 URL, it should at least have three groups.
  MOZ_ASSERT_IF(mIPGroup > 0, mIPGroup >= 3);

  nsAutoCString spec(mScheme);

  if (mSchemeType == eFile) {
    spec.AppendLiteral("://");

    if (mUniversalFileOrigin) {
      MOZ_ASSERT(mPathnameComponents.Length() == 1);

      spec.Append(mPathnameComponents[0]);
    } else {
      for (uint32_t count = mPathnameComponents.Length(), index = 0;
           index < count; index++) {
        spec.Append('/');
        spec.Append(mPathnameComponents[index]);
      }
    }

    aSpec = spec;

    return ValidOrigin;
  }

  if (mSchemeType == eAbout) {
    if (mMaybeObsolete) {
      // The "moz-safe-about+++home" was acciedntally created by a buggy nightly
      // and can be safely removed.
      return mHost.EqualsLiteral("home") ? ObsoleteOrigin : InvalidOrigin;
    }
    spec.Append(':');
  } else if (mSchemeType != eChrome) {
    spec.AppendLiteral("://");
  }

  spec.Append(mHost);

  if (!mPort.IsNull()) {
    spec.Append(':');
    spec.AppendInt(mPort.Value());
  }

  aSpec = spec;

  return mScheme.EqualsLiteral("app") ? ObsoleteOrigin : ValidOrigin;
}

void OriginParser::HandleScheme(const nsDependentCSubstring& aToken) {
  MOZ_ASSERT(!aToken.IsEmpty());
  MOZ_ASSERT(mState == eExpectingAppIdOrScheme || mState == eExpectingScheme);

  bool isAbout = false;
  bool isMozSafeAbout = false;
  bool isFile = false;
  bool isChrome = false;
  if (aToken.EqualsLiteral("http") || aToken.EqualsLiteral("https") ||
      (isAbout = aToken.EqualsLiteral("about") ||
                 (isMozSafeAbout = aToken.EqualsLiteral("moz-safe-about"))) ||
      aToken.EqualsLiteral("indexeddb") ||
      (isFile = aToken.EqualsLiteral("file")) || aToken.EqualsLiteral("app") ||
      aToken.EqualsLiteral("resource") ||
      aToken.EqualsLiteral("moz-extension") ||
      (isChrome = aToken.EqualsLiteral(kChromeOrigin)) ||
      aToken.EqualsLiteral("uuid")) {
    mScheme = aToken;

    if (isAbout) {
      mSchemeType = eAbout;
      mState = isMozSafeAbout ? eExpectingEmptyToken1OrHost : eExpectingHost;
    } else if (isChrome) {
      mSchemeType = eChrome;
      if (mTokenizer.hasMoreTokens()) {
        mError = true;
      }
      mState = eComplete;
    } else {
      if (isFile) {
        mSchemeType = eFile;
      }
      mState = eExpectingEmptyToken1;
    }

    return;
  }

  QM_WARNING("'%s' is not a valid scheme!", nsCString(aToken).get());

  mError = true;
}

void OriginParser::HandlePathnameComponent(
    const nsDependentCSubstring& aToken) {
  MOZ_ASSERT(!aToken.IsEmpty());
  MOZ_ASSERT(mState == eExpectingEmptyTokenOrDriveLetterOrPathnameComponent ||
             mState == eExpectingEmptyTokenOrPathnameComponent);
  MOZ_ASSERT(mSchemeType == eFile);

  mPathnameComponents.AppendElement(aToken);

  mState = mTokenizer.hasMoreTokens() ? eExpectingEmptyTokenOrPathnameComponent
                                      : eComplete;
}

void OriginParser::HandleToken(const nsDependentCSubstring& aToken) {
  switch (mState) {
    case eExpectingAppIdOrScheme: {
      if (aToken.IsEmpty()) {
        QM_WARNING("Expected an app id or scheme (not an empty string)!");

        mError = true;
        return;
      }

      if (IsAsciiDigit(aToken.First())) {
        // nsDependentCSubstring doesn't provice ToInteger()
        nsCString token(aToken);

        nsresult rv;
        Unused << token.ToInteger(&rv);
        if (NS_SUCCEEDED(rv)) {
          mState = eExpectingInMozBrowser;
          return;
        }
      }

      HandleScheme(aToken);

      return;
    }

    case eExpectingInMozBrowser: {
      if (aToken.Length() != 1) {
        QM_WARNING("'%zu' is not a valid length for the inMozBrowser flag!",
                   aToken.Length());

        mError = true;
        return;
      }

      if (aToken.First() == 't') {
        mInIsolatedMozBrowser = true;
      } else if (aToken.First() == 'f') {
        mInIsolatedMozBrowser = false;
      } else {
        QM_WARNING("'%s' is not a valid value for the inMozBrowser flag!",
                   nsCString(aToken).get());

        mError = true;
        return;
      }

      mState = eExpectingScheme;

      return;
    }

    case eExpectingScheme: {
      if (aToken.IsEmpty()) {
        QM_WARNING("Expected a scheme (not an empty string)!");

        mError = true;
        return;
      }

      HandleScheme(aToken);

      return;
    }

    case eExpectingEmptyToken1: {
      if (!aToken.IsEmpty()) {
        QM_WARNING("Expected the first empty token!");

        mError = true;
        return;
      }

      mState = eExpectingEmptyToken2;

      return;
    }

    case eExpectingEmptyToken2: {
      if (!aToken.IsEmpty()) {
        QM_WARNING("Expected the second empty token!");

        mError = true;
        return;
      }

      if (mSchemeType == eFile) {
        mState = eExpectingEmptyTokenOrUniversalFileOrigin;
      } else {
        if (mSchemeType == eAbout) {
          mMaybeObsolete = true;
        }
        mState = eExpectingHost;
      }

      return;
    }

    case eExpectingEmptyTokenOrUniversalFileOrigin: {
      MOZ_ASSERT(mSchemeType == eFile);

      if (aToken.IsEmpty()) {
        mState = mTokenizer.hasMoreTokens()
                     ? eExpectingEmptyTokenOrDriveLetterOrPathnameComponent
                     : eComplete;

        return;
      }

      if (aToken.EqualsLiteral("UNIVERSAL_FILE_URI_ORIGIN")) {
        mUniversalFileOrigin = true;

        mPathnameComponents.AppendElement(aToken);

        mState = eComplete;

        return;
      }

      QM_WARNING(
          "Expected the third empty token or "
          "UNIVERSAL_FILE_URI_ORIGIN!");

      mError = true;
      return;
    }

    case eExpectingHost: {
      if (aToken.IsEmpty()) {
        QM_WARNING("Expected a host (not an empty string)!");

        mError = true;
        return;
      }

      mHost = aToken;

      if (aToken.First() == '[') {
        MOZ_ASSERT(mIPGroup == 0);

        ++mIPGroup;
        mState = eExpectingIPV6Token;

        MOZ_ASSERT(mTokenizer.hasMoreTokens());
        return;
      }

      if (mTokenizer.hasMoreTokens()) {
        if (mSchemeType == eAbout) {
          QM_WARNING("Expected an empty string after host!");

          mError = true;
          return;
        }

        mState = eExpectingPort;

        return;
      }

      mState = eComplete;

      return;
    }

    case eExpectingPort: {
      MOZ_ASSERT(mSchemeType == eNone);

      if (aToken.IsEmpty()) {
        QM_WARNING("Expected a port (not an empty string)!");

        mError = true;
        return;
      }

      // nsDependentCSubstring doesn't provice ToInteger()
      nsCString token(aToken);

      nsresult rv;
      uint32_t port = token.ToInteger(&rv);
      if (NS_SUCCEEDED(rv)) {
        mPort.SetValue() = port;
      } else {
        QM_WARNING("'%s' is not a valid port number!", token.get());

        mError = true;
        return;
      }

      mState = eComplete;

      return;
    }

    case eExpectingEmptyTokenOrDriveLetterOrPathnameComponent: {
      MOZ_ASSERT(mSchemeType == eFile);

      if (aToken.IsEmpty()) {
        mPathnameComponents.AppendElement(""_ns);

        mState = mTokenizer.hasMoreTokens()
                     ? eExpectingEmptyTokenOrPathnameComponent
                     : eComplete;

        return;
      }

      if (aToken.Length() == 1 && IsAsciiAlpha(aToken.First())) {
        mMaybeDriveLetter = true;

        mPathnameComponents.AppendElement(aToken);

        mState = mTokenizer.hasMoreTokens()
                     ? eExpectingEmptyTokenOrPathnameComponent
                     : eComplete;

        return;
      }

      HandlePathnameComponent(aToken);

      return;
    }

    case eExpectingEmptyTokenOrPathnameComponent: {
      MOZ_ASSERT(mSchemeType == eFile);

      if (aToken.IsEmpty()) {
        if (mMaybeDriveLetter) {
          MOZ_ASSERT(mPathnameComponents.Length() == 1);

          nsCString& pathnameComponent = mPathnameComponents[0];
          pathnameComponent.Append(':');

          mMaybeDriveLetter = false;
        } else {
          mPathnameComponents.AppendElement(""_ns);
        }

        mState = mTokenizer.hasMoreTokens()
                     ? eExpectingEmptyTokenOrPathnameComponent
                     : eComplete;

        return;
      }

      HandlePathnameComponent(aToken);

      return;
    }

    case eExpectingEmptyToken1OrHost: {
      MOZ_ASSERT(mSchemeType == eAbout &&
                 mScheme.EqualsLiteral("moz-safe-about"));

      if (aToken.IsEmpty()) {
        mState = eExpectingEmptyToken2;
      } else {
        mHost = aToken;
        mState = mTokenizer.hasMoreTokens() ? eExpectingPort : eComplete;
      }

      return;
    }

    case eExpectingIPV6Token: {
      // A safe check for preventing infinity recursion.
      if (++mIPGroup > 8) {
        mError = true;
        return;
      }

      mHost.AppendLiteral(":");
      mHost.Append(aToken);
      if (!aToken.IsEmpty() && aToken.Last() == ']') {
        mState = mTokenizer.hasMoreTokens() ? eExpectingPort : eComplete;
      }

      return;
    }

    default:
      MOZ_CRASH("Should never get here!");
  }
}

void OriginParser::HandleTrailingSeparator() {
  MOZ_ASSERT(mState == eComplete);
  MOZ_ASSERT(mSchemeType == eFile);

  mPathnameComponents.AppendElement(""_ns);

  mState = eHandledTrailingSeparator;
}

nsresult RepositoryOperationBase::ProcessRepository() {
  AssertIsOnIOThread();

#ifdef DEBUG
  {
    QM_TRY_INSPECT(const bool& exists,
                   MOZ_TO_RESULT_INVOKE_MEMBER(mDirectory, Exists),
                   QM_ASSERT_UNREACHABLE);
    MOZ_ASSERT(exists);
  }
#endif

  QM_TRY(CollectEachFileEntry(
      *mDirectory,
      [](const auto& originFile) -> Result<mozilla::Ok, nsresult> {
        QM_TRY_INSPECT(const auto& leafName,
                       MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
                           nsAutoString, originFile, GetLeafName));

        // Unknown files during upgrade are allowed. Just warn if we find
        // them.
        if (!IsOSMetadata(leafName)) {
          UNKNOWN_FILE_WARNING(leafName);
        }

        return mozilla::Ok{};
      },
      [&self = *this](const auto& originDir) -> Result<mozilla::Ok, nsresult> {
        OriginProps originProps(WrapMovingNotNullUnchecked(originDir));
        QM_TRY(MOZ_TO_RESULT(originProps.Init([&self](const auto& aSpec) {
          return self.PersistenceTypeFromSpec(aSpec);
        })));
        // Bypass invalid origins while upgrading
        QM_TRY(OkIf(originProps.mType != OriginProps::eInvalid), mozilla::Ok{});

        if (originProps.mType != OriginProps::eObsolete) {
          QM_TRY_INSPECT(const bool& removed,
                         MOZ_TO_RESULT_INVOKE_MEMBER(
                             self, PrepareOriginDirectory, originProps));
          if (removed) {
            return mozilla::Ok{};
          }
        }

        self.mOriginProps.AppendElement(std::move(originProps));

        return mozilla::Ok{};
      }));

  if (mOriginProps.IsEmpty()) {
    return NS_OK;
  }

  QM_TRY(MOZ_TO_RESULT(ProcessOriginDirectories()));

  return NS_OK;
}

template <typename UpgradeMethod>
nsresult RepositoryOperationBase::MaybeUpgradeClients(
    const OriginProps& aOriginProps, UpgradeMethod aMethod) {
  AssertIsOnIOThread();
  MOZ_ASSERT(aMethod);

  QuotaManager* quotaManager = QuotaManager::Get();
  MOZ_ASSERT(quotaManager);

  QM_TRY(CollectEachFileEntry(
      *aOriginProps.mDirectory,
      [](const auto& file) -> Result<mozilla::Ok, nsresult> {
        QM_TRY_INSPECT(
            const auto& leafName,
            MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoString, file, GetLeafName));

        if (!IsOriginMetadata(leafName) && !IsTempMetadata(leafName)) {
          UNKNOWN_FILE_WARNING(leafName);
        }

        return mozilla::Ok{};
      },
      [quotaManager, &aMethod,
       &self = *this](const auto& dir) -> Result<mozilla::Ok, nsresult> {
        QM_TRY_INSPECT(
            const auto& leafName,
            MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoString, dir, GetLeafName));

        QM_TRY_INSPECT(const bool& removed,
                       MOZ_TO_RESULT_INVOKE_MEMBER(self, PrepareClientDirectory,
                                                   dir, leafName));
        if (removed) {
          return mozilla::Ok{};
        }

        Client::Type clientType;
        bool ok = Client::TypeFromText(leafName, clientType, fallible);
        if (!ok) {
          UNKNOWN_FILE_WARNING(leafName);
          return mozilla::Ok{};
        }

        Client* client = quotaManager->GetClient(clientType);
        MOZ_ASSERT(client);

        QM_TRY(MOZ_TO_RESULT((client->*aMethod)(dir)));

        return mozilla::Ok{};
      }));

  return NS_OK;
}

nsresult RepositoryOperationBase::PrepareClientDirectory(
    nsIFile* aFile, const nsAString& aLeafName, bool& aRemoved) {
  AssertIsOnIOThread();

  aRemoved = false;
  return NS_OK;
}

nsresult CreateOrUpgradeDirectoryMetadataHelper::Init() {
  AssertIsOnIOThread();
  MOZ_ASSERT(mDirectory);

  const auto maybeLegacyPersistenceType =
      LegacyPersistenceTypeFromFile(*mDirectory, fallible);
  QM_TRY(OkIf(maybeLegacyPersistenceType.isSome()), Err(NS_ERROR_FAILURE));

  mLegacyPersistenceType.init(maybeLegacyPersistenceType.value());

  return NS_OK;
}

Maybe<CreateOrUpgradeDirectoryMetadataHelper::LegacyPersistenceType>
CreateOrUpgradeDirectoryMetadataHelper::LegacyPersistenceTypeFromFile(
    nsIFile& aFile, const fallible_t&) {
  nsAutoString leafName;
  MOZ_ALWAYS_SUCCEEDS(aFile.GetLeafName(leafName));

  if (leafName.Equals(u"persistent"_ns)) {
    return Some(LegacyPersistenceType::Persistent);
  }

  if (leafName.Equals(u"temporary"_ns)) {
    return Some(LegacyPersistenceType::Temporary);
  }

  return Nothing();
}

PersistenceType
CreateOrUpgradeDirectoryMetadataHelper::PersistenceTypeFromLegacyPersistentSpec(
    const nsCString& aSpec) {
  if (QuotaManager::IsOriginInternal(aSpec)) {
    return PERSISTENCE_TYPE_PERSISTENT;
  }

  return PERSISTENCE_TYPE_DEFAULT;
}

PersistenceType CreateOrUpgradeDirectoryMetadataHelper::PersistenceTypeFromSpec(
    const nsCString& aSpec) {
  switch (*mLegacyPersistenceType) {
    case LegacyPersistenceType::Persistent:
      return PersistenceTypeFromLegacyPersistentSpec(aSpec);
    case LegacyPersistenceType::Temporary:
      return PERSISTENCE_TYPE_TEMPORARY;
  }
  MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE("Bad legacy persistence type value!");
}

nsresult CreateOrUpgradeDirectoryMetadataHelper::MaybeUpgradeOriginDirectory(
    nsIFile* aDirectory) {
  AssertIsOnIOThread();
  MOZ_ASSERT(aDirectory);

  QM_TRY_INSPECT(
      const auto& metadataFile,
      CloneFileAndAppend(*aDirectory, nsLiteralString(METADATA_FILE_NAME)));

  QM_TRY_INSPECT(const bool& exists,
                 MOZ_TO_RESULT_INVOKE_MEMBER(metadataFile, Exists));

  if (!exists) {
    // Directory structure upgrade needed.
    // Move all files to IDB specific directory.

    nsString idbDirectoryName;
    QM_TRY(OkIf(Client::TypeToText(Client::IDB, idbDirectoryName, fallible)),
           NS_ERROR_FAILURE);

    QM_TRY_INSPECT(const auto& idbDirectory,
                   CloneFileAndAppend(*aDirectory, idbDirectoryName));

    // Usually we only use QM_OR_ELSE_LOG_VERBOSE/QM_OR_ELSE_LOG_VERBOSE_IF
    // with Create and NS_ERROR_FILE_ALREADY_EXISTS check, but typically the
    // idb directory shouldn't exist during the upgrade and the upgrade runs
    // only once in most of the cases, so the use of QM_OR_ELSE_WARN_IF is ok
    // here.
    QM_TRY(QM_OR_ELSE_WARN_IF(
        // Expression.
        MOZ_TO_RESULT(idbDirectory->Create(nsIFile::DIRECTORY_TYPE, 0755)),
        // Predicate.
        IsSpecificError<NS_ERROR_FILE_ALREADY_EXISTS>,
        // Fallback.
        ([&idbDirectory](const nsresult rv) -> Result<Ok, nsresult> {
          QM_TRY_INSPECT(
              const bool& isDirectory,
              MOZ_TO_RESULT_INVOKE_MEMBER(idbDirectory, IsDirectory));

          QM_TRY(OkIf(isDirectory), Err(NS_ERROR_UNEXPECTED));

          return Ok{};
        })));

    QM_TRY(CollectEachFile(
        *aDirectory,
        [&idbDirectory, &idbDirectoryName](
            const nsCOMPtr<nsIFile>& file) -> Result<Ok, nsresult> {
          QM_TRY_INSPECT(const auto& leafName,
                         MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoString, file,
                                                           GetLeafName));

          if (!leafName.Equals(idbDirectoryName)) {
            QM_TRY(MOZ_TO_RESULT(file->MoveTo(idbDirectory, u""_ns)));
          }

          return Ok{};
        }));

    QM_TRY(
        MOZ_TO_RESULT(metadataFile->Create(nsIFile::NORMAL_FILE_TYPE, 0644)));
  }

  return NS_OK;
}

nsresult CreateOrUpgradeDirectoryMetadataHelper::PrepareOriginDirectory(
    OriginProps& aOriginProps, bool* aRemoved) {
  AssertIsOnIOThread();
  MOZ_ASSERT(aRemoved);

  if (*mLegacyPersistenceType == LegacyPersistenceType::Persistent) {
    QM_TRY(MOZ_TO_RESULT(
        MaybeUpgradeOriginDirectory(aOriginProps.mDirectory.get())));

    aOriginProps.mTimestamp = GetOriginLastModifiedTime(aOriginProps);
  } else {
    int64_t timestamp;
    nsCString group;
    nsCString origin;
    Nullable<bool> isApp;

    QM_WARNONLY_TRY_UNWRAP(
        const auto maybeDirectoryMetadata,
        MOZ_TO_RESULT(GetDirectoryMetadata(aOriginProps.mDirectory.get(),
                                           timestamp, group, origin, isApp)));
    if (!maybeDirectoryMetadata) {
      aOriginProps.mTimestamp = GetOriginLastModifiedTime(aOriginProps);
      aOriginProps.mNeedsRestore = true;
    } else if (!isApp.IsNull()) {
      aOriginProps.mIgnore = true;
    }
  }

  *aRemoved = false;
  return NS_OK;
}

nsresult CreateOrUpgradeDirectoryMetadataHelper::ProcessOriginDirectory(
    const OriginProps& aOriginProps) {
  AssertIsOnIOThread();

  if (*mLegacyPersistenceType == LegacyPersistenceType::Persistent) {
    QM_TRY(MOZ_TO_RESULT(QuotaManager::CreateDirectoryMetadata(
        *aOriginProps.mDirectory, aOriginProps.mTimestamp,
        aOriginProps.mOriginMetadata)));

    // Move internal origins to new persistent storage.
    if (PersistenceTypeFromLegacyPersistentSpec(aOriginProps.mSpec) ==
        PERSISTENCE_TYPE_PERSISTENT) {
      if (!mPermanentStorageDir) {
        QuotaManager* quotaManager = QuotaManager::Get();
        MOZ_ASSERT(quotaManager);

        const nsString& permanentStoragePath =
            quotaManager->GetStoragePath(PERSISTENCE_TYPE_PERSISTENT);

        QM_TRY_UNWRAP(mPermanentStorageDir,
                      QM_NewLocalFile(permanentStoragePath));
      }

      const nsAString& leafName = aOriginProps.mLeafName;

      QM_TRY_INSPECT(const auto& newDirectory,
                     CloneFileAndAppend(*mPermanentStorageDir, leafName));

      QM_TRY_INSPECT(const bool& exists,
                     MOZ_TO_RESULT_INVOKE_MEMBER(newDirectory, Exists));

      if (exists) {
        QM_WARNING("Found %s in storage/persistent and storage/permanent !",
                   NS_ConvertUTF16toUTF8(leafName).get());

        QM_TRY(MOZ_TO_RESULT(
            aOriginProps.mDirectory->Remove(/* recursive */ true)));
      } else {
        QM_TRY(MOZ_TO_RESULT(
            aOriginProps.mDirectory->MoveTo(mPermanentStorageDir, u""_ns)));
      }
    }
  } else if (aOriginProps.mNeedsRestore) {
    QM_TRY(MOZ_TO_RESULT(QuotaManager::CreateDirectoryMetadata(
        *aOriginProps.mDirectory, aOriginProps.mTimestamp,
        aOriginProps.mOriginMetadata)));
  } else if (!aOriginProps.mIgnore) {
    QM_TRY_INSPECT(const auto& file,
                   CloneFileAndAppend(*aOriginProps.mDirectory,
                                      nsLiteralString(METADATA_FILE_NAME)));

    QM_TRY_INSPECT(const auto& stream,
                   GetBinaryOutputStream(*file, FileFlag::Append));

    MOZ_ASSERT(stream);

    // Currently unused (used to be isApp).
    QM_TRY(MOZ_TO_RESULT(stream->WriteBoolean(false)));
  }

  return NS_OK;
}

nsresult UpgradeStorageHelperBase::Init() {
  AssertIsOnIOThread();
  MOZ_ASSERT(mDirectory);

  const auto maybePersistenceType =
      PersistenceTypeFromFile(*mDirectory, fallible);
  QM_TRY(OkIf(maybePersistenceType.isSome()), Err(NS_ERROR_FAILURE));

  mPersistenceType.init(maybePersistenceType.value());

  return NS_OK;
}

PersistenceType UpgradeStorageHelperBase::PersistenceTypeFromSpec(
    const nsCString& aSpec) {
  // There's no moving of origin directories between repositories like in the
  // CreateOrUpgradeDirectoryMetadataHelper
  return *mPersistenceType;
}

nsresult UpgradeStorageFrom0_0To1_0Helper::PrepareOriginDirectory(
    OriginProps& aOriginProps, bool* aRemoved) {
  AssertIsOnIOThread();
  MOZ_ASSERT(aRemoved);

  int64_t timestamp;
  nsCString group;
  nsCString origin;
  Nullable<bool> isApp;

  QM_WARNONLY_TRY_UNWRAP(
      const auto maybeDirectoryMetadata,
      MOZ_TO_RESULT(GetDirectoryMetadata(aOriginProps.mDirectory.get(),
                                         timestamp, group, origin, isApp)));
  if (!maybeDirectoryMetadata || isApp.IsNull()) {
    aOriginProps.mTimestamp = GetOriginLastModifiedTime(aOriginProps);
    aOriginProps.mNeedsRestore = true;
  } else {
    aOriginProps.mTimestamp = timestamp;
  }

  *aRemoved = false;
  return NS_OK;
}

nsresult UpgradeStorageFrom0_0To1_0Helper::ProcessOriginDirectory(
    const OriginProps& aOriginProps) {
  AssertIsOnIOThread();

  // This handles changes in origin string generation from nsIPrincipal,
  // especially the change from: appId+inMozBrowser+originNoSuffix
  // to: origin (with origin suffix).
  QM_TRY_INSPECT(const bool& renamed, MaybeRenameOrigin(aOriginProps));
  if (renamed) {
    return NS_OK;
  }

  if (aOriginProps.mNeedsRestore) {
    QM_TRY(MOZ_TO_RESULT(QuotaManager::CreateDirectoryMetadata(
        *aOriginProps.mDirectory, aOriginProps.mTimestamp,
        aOriginProps.mOriginMetadata)));
  }

  QM_TRY(MOZ_TO_RESULT(QuotaManager::CreateDirectoryMetadata2(
      *aOriginProps.mDirectory, aOriginProps.mTimestamp,
      /* aPersisted */ false, aOriginProps.mOriginMetadata)));

  return NS_OK;
}

nsresult UpgradeStorageFrom1_0To2_0Helper::MaybeRemoveMorgueDirectory(
    const OriginProps& aOriginProps) {
  AssertIsOnIOThread();

  // The Cache API was creating top level morgue directories by accident for
  // a short time in nightly.  This unfortunately prevents all storage from
  // working.  So recover these profiles permanently by removing these corrupt
  // directories as part of this upgrade.

  QM_TRY_INSPECT(const auto& morgueDir,
                 MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
                     nsCOMPtr<nsIFile>, *aOriginProps.mDirectory, Clone));

  QM_TRY(MOZ_TO_RESULT(morgueDir->Append(u"morgue"_ns)));

  QM_TRY_INSPECT(const bool& exists,
                 MOZ_TO_RESULT_INVOKE_MEMBER(morgueDir, Exists));

  if (exists) {
    QM_WARNING("Deleting accidental morgue directory!");

    QM_TRY(MOZ_TO_RESULT(morgueDir->Remove(/* recursive */ true)));
  }

  return NS_OK;
}

Result<bool, nsresult> UpgradeStorageFrom1_0To2_0Helper::MaybeRemoveAppsData(
    const OriginProps& aOriginProps) {
  AssertIsOnIOThread();

  // TODO: This method was empty for some time due to accidental changes done
  //       in bug 1320404. This led to renaming of origin directories like:
  //         https+++developer.cdn.mozilla.net^appId=1007&inBrowser=1
  //       to:
  //         https+++developer.cdn.mozilla.net^inBrowser=1
  //       instead of just removing them.

  const nsCString& originalSuffix = aOriginProps.mOriginalSuffix;
  if (!originalSuffix.IsEmpty()) {
    MOZ_ASSERT(originalSuffix[0] == '^');

    if (!URLParams::Parse(
            Substring(originalSuffix, 1, originalSuffix.Length() - 1),
            [](const nsAString& aName, const nsAString& aValue) {
              if (aName.EqualsLiteral("appId")) {
                return false;
              }

              return true;
            })) {
      QM_TRY(MOZ_TO_RESULT(RemoveObsoleteOrigin(aOriginProps)));

      return true;
    }
  }

  return false;
}

nsresult UpgradeStorageFrom1_0To2_0Helper::PrepareOriginDirectory(
    OriginProps& aOriginProps, bool* aRemoved) {
  AssertIsOnIOThread();
  MOZ_ASSERT(aRemoved);

  QM_TRY(MOZ_TO_RESULT(MaybeRemoveMorgueDirectory(aOriginProps)));

  QM_TRY(MOZ_TO_RESULT(
      MaybeUpgradeClients(aOriginProps, &Client::UpgradeStorageFrom1_0To2_0)));

  QM_TRY_INSPECT(const bool& removed, MaybeRemoveAppsData(aOriginProps));
  if (removed) {
    *aRemoved = true;
    return NS_OK;
  }

  int64_t timestamp;
  nsCString group;
  nsCString origin;
  Nullable<bool> isApp;
  QM_WARNONLY_TRY_UNWRAP(
      const auto maybeDirectoryMetadata,
      MOZ_TO_RESULT(GetDirectoryMetadata(aOriginProps.mDirectory.get(),
                                         timestamp, group, origin, isApp)));
  if (!maybeDirectoryMetadata || isApp.IsNull()) {
    aOriginProps.mNeedsRestore = true;
  }

  nsCString suffix;
  QM_WARNONLY_TRY_UNWRAP(const auto maybeDirectoryMetadata2,
                         MOZ_TO_RESULT(GetDirectoryMetadata2(
                             aOriginProps.mDirectory.get(), timestamp, suffix,
                             group, origin, isApp.SetValue())));
  if (!maybeDirectoryMetadata2) {
    aOriginProps.mTimestamp = GetOriginLastModifiedTime(aOriginProps);
    aOriginProps.mNeedsRestore2 = true;
  } else {
    aOriginProps.mTimestamp = timestamp;
  }

  *aRemoved = false;
  return NS_OK;
}

nsresult UpgradeStorageFrom1_0To2_0Helper::ProcessOriginDirectory(
    const OriginProps& aOriginProps) {
  AssertIsOnIOThread();

  // This handles changes in origin string generation from nsIPrincipal,
  // especially the stripping of obsolete origin attributes like addonId.
  QM_TRY_INSPECT(const bool& renamed, MaybeRenameOrigin(aOriginProps));
  if (renamed) {
    return NS_OK;
  }

  if (aOriginProps.mNeedsRestore) {
    QM_TRY(MOZ_TO_RESULT(QuotaManager::CreateDirectoryMetadata(
        *aOriginProps.mDirectory, aOriginProps.mTimestamp,
        aOriginProps.mOriginMetadata)));
  }

  if (aOriginProps.mNeedsRestore2) {
    QM_TRY(MOZ_TO_RESULT(QuotaManager::CreateDirectoryMetadata2(
        *aOriginProps.mDirectory, aOriginProps.mTimestamp,
        /* aPersisted */ false, aOriginProps.mOriginMetadata)));
  }

  return NS_OK;
}

nsresult UpgradeStorageFrom2_0To2_1Helper::PrepareOriginDirectory(
    OriginProps& aOriginProps, bool* aRemoved) {
  AssertIsOnIOThread();
  MOZ_ASSERT(aRemoved);

  QM_TRY(MOZ_TO_RESULT(
      MaybeUpgradeClients(aOriginProps, &Client::UpgradeStorageFrom2_0To2_1)));

  int64_t timestamp;
  nsCString group;
  nsCString origin;
  Nullable<bool> isApp;
  QM_WARNONLY_TRY_UNWRAP(
      const auto maybeDirectoryMetadata,
      MOZ_TO_RESULT(GetDirectoryMetadata(aOriginProps.mDirectory.get(),
                                         timestamp, group, origin, isApp)));
  if (!maybeDirectoryMetadata || isApp.IsNull()) {
    aOriginProps.mNeedsRestore = true;
  }

  nsCString suffix;
  QM_WARNONLY_TRY_UNWRAP(const auto maybeDirectoryMetadata2,
                         MOZ_TO_RESULT(GetDirectoryMetadata2(
                             aOriginProps.mDirectory.get(), timestamp, suffix,
                             group, origin, isApp.SetValue())));
  if (!maybeDirectoryMetadata2) {
    aOriginProps.mTimestamp = GetOriginLastModifiedTime(aOriginProps);
    aOriginProps.mNeedsRestore2 = true;
  } else {
    aOriginProps.mTimestamp = timestamp;
  }

  *aRemoved = false;
  return NS_OK;
}

nsresult UpgradeStorageFrom2_0To2_1Helper::ProcessOriginDirectory(
    const OriginProps& aOriginProps) {
  AssertIsOnIOThread();

  if (aOriginProps.mNeedsRestore) {
    QM_TRY(MOZ_TO_RESULT(QuotaManager::CreateDirectoryMetadata(
        *aOriginProps.mDirectory, aOriginProps.mTimestamp,
        aOriginProps.mOriginMetadata)));
  }

  if (aOriginProps.mNeedsRestore2) {
    QM_TRY(MOZ_TO_RESULT(QuotaManager::CreateDirectoryMetadata2(
        *aOriginProps.mDirectory, aOriginProps.mTimestamp,
        /* aPersisted */ false, aOriginProps.mOriginMetadata)));
  }

  return NS_OK;
}

nsresult UpgradeStorageFrom2_1To2_2Helper::PrepareOriginDirectory(
    OriginProps& aOriginProps, bool* aRemoved) {
  AssertIsOnIOThread();
  MOZ_ASSERT(aRemoved);

  QM_TRY(MOZ_TO_RESULT(
      MaybeUpgradeClients(aOriginProps, &Client::UpgradeStorageFrom2_1To2_2)));

  int64_t timestamp;
  nsCString group;
  nsCString origin;
  Nullable<bool> isApp;
  QM_WARNONLY_TRY_UNWRAP(
      const auto maybeDirectoryMetadata,
      MOZ_TO_RESULT(GetDirectoryMetadata(aOriginProps.mDirectory.get(),
                                         timestamp, group, origin, isApp)));
  if (!maybeDirectoryMetadata || isApp.IsNull()) {
    aOriginProps.mNeedsRestore = true;
  }

  nsCString suffix;
  QM_WARNONLY_TRY_UNWRAP(const auto maybeDirectoryMetadata2,
                         MOZ_TO_RESULT(GetDirectoryMetadata2(
                             aOriginProps.mDirectory.get(), timestamp, suffix,
                             group, origin, isApp.SetValue())));
  if (!maybeDirectoryMetadata2) {
    aOriginProps.mTimestamp = GetOriginLastModifiedTime(aOriginProps);
    aOriginProps.mNeedsRestore2 = true;
  } else {
    aOriginProps.mTimestamp = timestamp;
  }

  *aRemoved = false;
  return NS_OK;
}

nsresult UpgradeStorageFrom2_1To2_2Helper::ProcessOriginDirectory(
    const OriginProps& aOriginProps) {
  AssertIsOnIOThread();

  if (aOriginProps.mNeedsRestore) {
    QM_TRY(MOZ_TO_RESULT(QuotaManager::CreateDirectoryMetadata(
        *aOriginProps.mDirectory, aOriginProps.mTimestamp,
        aOriginProps.mOriginMetadata)));
  }

  if (aOriginProps.mNeedsRestore2) {
    QM_TRY(MOZ_TO_RESULT(QuotaManager::CreateDirectoryMetadata2(
        *aOriginProps.mDirectory, aOriginProps.mTimestamp,
        /* aPersisted */ false, aOriginProps.mOriginMetadata)));
  }

  return NS_OK;
}

nsresult UpgradeStorageFrom2_1To2_2Helper::PrepareClientDirectory(
    nsIFile* aFile, const nsAString& aLeafName, bool& aRemoved) {
  AssertIsOnIOThread();

  if (Client::IsDeprecatedClient(aLeafName)) {
    QM_WARNING("Deleting deprecated %s client!",
               NS_ConvertUTF16toUTF8(aLeafName).get());

    QM_TRY(MOZ_TO_RESULT(aFile->Remove(true)));

    aRemoved = true;
  } else {
    aRemoved = false;
  }

  return NS_OK;
}

nsresult RestoreDirectoryMetadata2Helper::Init() {
  AssertIsOnIOThread();
  MOZ_ASSERT(mDirectory);

  nsCOMPtr<nsIFile> parentDir;
  QM_TRY(MOZ_TO_RESULT(mDirectory->GetParent(getter_AddRefs(parentDir))));

  const auto maybePersistenceType =
      PersistenceTypeFromFile(*parentDir, fallible);
  QM_TRY(OkIf(maybePersistenceType.isSome()), Err(NS_ERROR_FAILURE));

  mPersistenceType.init(maybePersistenceType.value());

  return NS_OK;
}

nsresult RestoreDirectoryMetadata2Helper::RestoreMetadata2File() {
  OriginProps originProps(WrapMovingNotNull(mDirectory));
  QM_TRY(MOZ_TO_RESULT(originProps.Init(
      [&self = *this](const auto& aSpec) { return *self.mPersistenceType; })));

  QM_TRY(OkIf(originProps.mType != OriginProps::eInvalid), NS_ERROR_FAILURE);

  originProps.mTimestamp = GetOriginLastModifiedTime(originProps);

  mOriginProps.AppendElement(std::move(originProps));

  QM_TRY(MOZ_TO_RESULT(ProcessOriginDirectories()));

  return NS_OK;
}

nsresult RestoreDirectoryMetadata2Helper::ProcessOriginDirectory(
    const OriginProps& aOriginProps) {
  AssertIsOnIOThread();

  // We don't have any approach to restore aPersisted, so reset it to false.
  QM_TRY(MOZ_TO_RESULT(QuotaManager::CreateDirectoryMetadata2(
      *aOriginProps.mDirectory, aOriginProps.mTimestamp,
      /* aPersisted */ false, aOriginProps.mOriginMetadata)));

  return NS_OK;
}

}  // namespace mozilla::dom::quota
