dom/quota/ActorsParent.cpp
author Jan Varga <jvarga@mozilla.com>
Fri, 25 Jun 2021 06:27:59 +0000
changeset 584359 68f759c171f29cd56e583d5a08276607aeaadde7
parent 584227 eef282671a2b52f39acb79d6044d0e778fd0734f
child 584390 d70cf6ab3f6228f57cd181d294b260601bfaa66e
permissions -rw-r--r--
Bug 1717990 - QM: Convert some NS_ERROR_FAILURE which should have been NS_ERROR_ABORT; r=dom-storage-reviewers,jstutte Differential Revision: https://phabricator.services.mozilla.com/D118676

/* -*- 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 "Flatten.h"
#include "FirstInitializationAttemptsImpl.h"
#include "OriginScope.h"
#include "QuotaCommon.h"
#include "QuotaManager.h"
#include "QuotaObject.h"
#include "UsageInfo.h"

// Global includes
#include <cinttypes>
#include <cstdlib>
#include <cstring>
#include <algorithm>
#include <cstdint>
#include <functional>
#include <new>
#include <numeric>
#include <tuple>
#include <type_traits>
#include <utility>
#include "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/ProfilerLabels.h"
#include "mozilla/RefPtr.h"
#include "mozilla/Result.h"
#include "mozilla/ResultExtensions.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/Services.h"
#include "mozilla/SpinEventLoopUntil.h"
#include "mozilla/StaticPrefs_dom.h"
#include "mozilla/StaticPtr.h"
#include "mozilla/Telemetry.h"
#include "mozilla/TelemetryHistogramEnums.h"
#include "mozilla/TextUtils.h"
#include "mozilla/TimeStamp.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/Unused.h"
#include "mozilla/Variant.h"
#include "mozilla/dom/FlippedOnce.h"
#include "mozilla/dom/LocalStorageCommon.h"
#include "mozilla/dom/StorageActivityService.h"
#include "mozilla/dom/StorageDBUpdater.h"
#include "mozilla/dom/StorageTypeBinding.h"
#include "mozilla/dom/cache/QuotaClient.h"
#include "mozilla/dom/indexedDB/ActorsParent.h"
#include "mozilla/dom/ipc/IdType.h"
#include "mozilla/dom/localstorage/ActorsParent.h"
#include "mozilla/dom/QMResultInlines.h"
#include "mozilla/dom/quota/CheckedUnsafePtr.h"
#include "mozilla/dom/quota/Client.h"
#include "mozilla/dom/quota/DirectoryLock.h"
#include "mozilla/dom/quota/PersistenceType.h"
#include "mozilla/dom/quota/PQuota.h"
#include "mozilla/dom/quota/PQuotaParent.h"
#include "mozilla/dom/quota/PQuotaRequest.h"
#include "mozilla/dom/quota/PQuotaRequestParent.h"
#include "mozilla/dom/quota/PQuotaUsageRequest.h"
#include "mozilla/dom/quota/PQuotaUsageRequestParent.h"
#include "mozilla/dom/simpledb/ActorsParent.h"
#include "mozilla/fallible.h"
#include "mozilla/ipc/BackgroundChild.h"
#include "mozilla/ipc/BackgroundParent.h"
#include "mozilla/ipc/PBackgroundChild.h"
#include "mozilla/ipc/PBackgroundSharedTypes.h"
#include "mozilla/ipc/ProtocolUtils.h"
#include "mozilla/net/MozURL.h"
#include "nsAppDirectoryServiceDefs.h"
#include "nsBaseHashtable.h"
#include "nsCOMPtr.h"
#include "nsCRTGlue.h"
#include "nsCharSeparatedTokenizer.h"
#include "nsClassHashtable.h"
#include "nsComponentManagerUtils.h"
#include "nsContentUtils.h"
#include "nsTHashMap.h"
#include "nsDebug.h"
#include "nsDirectoryServiceUtils.h"
#include "nsError.h"
#include "nsHashKeys.h"
#include "nsIBinaryInputStream.h"
#include "nsIBinaryOutputStream.h"
#include "nsIConsoleService.h"
#include "nsIDirectoryEnumerator.h"
#include "nsIEventTarget.h"
#include "nsIFile.h"
#include "nsIFileStreams.h"
#include "nsIInputStream.h"
#include "nsIObjectInputStream.h"
#include "nsIObjectOutputStream.h"
#include "nsIObserver.h"
#include "nsIObserverService.h"
#include "nsIOutputStream.h"
#include "nsIPlatformInfo.h"
#include "nsIPrincipal.h"
#include "nsIRunnable.h"
#include "nsIScriptObjectPrincipal.h"
#include "nsISupports.h"
#include "nsISupportsPrimitives.h"
#include "nsIThread.h"
#include "nsITimer.h"
#include "nsIURI.h"
#include "nsIWidget.h"
#include "nsLiteralString.h"
#include "nsNetUtil.h"
#include "nsPIDOMWindow.h"
#include "nsPrintfCString.h"
#include "nsServiceManagerUtils.h"
#include "nsString.h"
#include "nsStringFlags.h"
#include "nsStringFwd.h"
#include "nsTArray.h"
#include "nsTHashtable.h"
#include "nsTLiteralString.h"
#include "nsTPromiseFlatString.h"
#include "nsTStringRepr.h"
#include "nsThreadUtils.h"
#include "nsURLHelper.h"
#include "nsXPCOM.h"
#include "nsXPCOMCID.h"
#include "nsXULAppAPI.h"
#include "prinrval.h"
#include "prio.h"
#include "prthread.h"
#include "prtime.h"

#define DISABLE_ASSERTS_FOR_FUZZING 0

#if DISABLE_ASSERTS_FOR_FUZZING
#  define ASSERT_UNLESS_FUZZING(...) \
    do {                             \
    } while (0)
#else
#  define ASSERT_UNLESS_FUZZING(...) MOZ_ASSERT(false, __VA_ARGS__)
#endif

// As part of bug 1536596 in order to identify the remaining sources of
// principal info inconsistencies, we have added anonymized crash logging and
// are temporarily making these checks occur on both debug and optimized
// nightly, dev-edition, and early beta builds through use of
// EARLY_BETA_OR_EARLIER during Firefox 82.  The plan is to return this
// condition to MOZ_DIAGNOSTIC_ASSERT_ENABLED during Firefox 84 at the latest.
// The analysis and disabling is tracked by bug 1536596.

#ifdef EARLY_BETA_OR_EARLIER
#  define QM_PRINCIPALINFO_VERIFICATION_ENABLED
#endif

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

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

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

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

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

namespace mozilla::dom::quota {

using namespace mozilla::ipc;
using mozilla::net::MozURL;

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

namespace {

template <typename T>
void AssertNoOverflow(uint64_t aDest, T aArg);

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

const uint32_t kSQLitePageSizeOverride = 512;

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

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

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

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

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

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

static_assert(static_cast<uint32_t>(StorageType::Persistent) ==
                  static_cast<uint32_t>(PERSISTENCE_TYPE_PERSISTENT),
              "Enum values should match.");

static_assert(static_cast<uint32_t>(StorageType::Temporary) ==
                  static_cast<uint32_t>(PERSISTENCE_TYPE_TEMPORARY),
              "Enum values should match.");

static_assert(static_cast<uint32_t>(StorageType::Default) ==
                  static_cast<uint32_t>(PERSISTENCE_TYPE_DEFAULT),
              "Enum values should match.");

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

constexpr auto kStorageName = u"storage"_ns;
constexpr auto kSQLiteSuffix = u".sqlite"_ns;

#define INDEXEDDB_DIRECTORY_NAME u"indexedDB"
#define PERSISTENT_DIRECTORY_NAME u"persistent"
#define PERMANENT_DIRECTORY_NAME u"permanent"
#define TEMPORARY_DIRECTORY_NAME u"temporary"
#define DEFAULT_DIRECTORY_NAME u"default"

// The name of the file that we use to load/save the last access time of an
// origin.
// XXX We should get rid of old metadata files at some point, bug 1343576.
#define METADATA_FILE_NAME u".metadata"
#define METADATA_TMP_FILE_NAME u".metadata-tmp"
#define METADATA_V2_FILE_NAME u".metadata-v2"
#define METADATA_V2_TMP_FILE_NAME u".metadata-v2-tmp"

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

const int32_t kLocalStorageArchiveVersion = 4;

const char kProfileDoChangeTopic[] = "profile-do-change";

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(
      aConnection->ExecuteSimpleSQL("CREATE TABLE database"
                                    "( cache_version INTEGER NOT NULL DEFAULT 0"
                                    ");"_ns));

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

    MOZ_ASSERT(storageVersion == 0);
  }
#endif

  QM_TRY(aConnection->SetSchemaVersion(kStorageVersion));

  return NS_OK;
}

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

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

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

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

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

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

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

  QM_TRY(stmt->Execute());

  return NS_OK;
}

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

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

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

  // Table `origin`
  QM_TRY(
      aConnection.ExecuteSimpleSQL("CREATE TABLE origin"
                                   "( repository_id INTEGER NOT NULL"
                                   ", 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(SaveCacheVersion(aConnection, kCacheVersion));

  return NS_OK;
}

nsresult 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.
      ([&]() -> Result<Ok, nsresult> {
        mozStorageTransaction transaction(&aConnection, false);

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

        return Ok{};
      }()),
      // Fallback.
      ([&](const nsresult rv) -> Result<Ok, nsresult> {
        QM_TRY(aConnection.ExecuteSimpleSQL(kSetInvalidFlagQuery));

        return Ok{};
      })));
  return NS_OK;
}

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

  QM_TRY(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(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(transaction.Start());

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

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

      QM_TRY(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_TYPED(
                                        nsCOMPtr<mozIStorageStatement>,
                                        aConnection, CreateStatement,
                                        "INSERT INTO repository (id, name) "
                                        "VALUES (:id, :name)"_ns));
        }

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

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

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

      while (cacheVersion != kCacheVersion) {
        if (cacheVersion == 1) {
          QM_TRY(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(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(aWebAppsStoreFile, Exists));

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

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

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

  QM_TRY_INSPECT(const auto& connection,
                 QM_OR_ELSE_WARN_IF(
                     // Expression.
                     MOZ_TO_RESULT_INVOKE_TYPED(
                         nsCOMPtr<mozIStorageConnection>, aStorageService,
                         OpenUnsharedDatabase, &aWebAppsStoreFile),
                     // 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(StorageDBUpdater::Update(connection),
           nsCOMPtr<mozIStorageConnection>{});
  }

  return connection;
}

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

  QM_TRY_UNWRAP(auto lsArchiveFile, QM_NewLocalFile(aDirectoryPath));

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

  return lsArchiveFile;
}

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

  QM_TRY_UNWRAP(auto lsArchiveTmpFile, QM_NewLocalFile(aDirectoryPath));

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

  return lsArchiveTmpFile;
}

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

  QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE(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(aConnection->ExecuteSimpleSQL(
      "CREATE TABLE database(version INTEGER NOT NULL DEFAULT 0);"_ns));

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

  QM_TRY(stmt->BindInt32ByName("version"_ns, 0));
  QM_TRY(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(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 void ShutdownCompleted();

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

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

  nsresult Init();

  nsresult Shutdown();

  NS_DECL_ISUPPORTS
  NS_DECL_NSIOBSERVER
};

namespace {

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

}  // namespace

// XXX Change this not to derive from AutoTArray.
class ClientUsageArray final
    : public AutoTArray<Maybe<uint64_t>, Client::TYPE_MAX> {
 public:
  ClientUsageArray() { SetLength(Client::TypeMax()); }

  void Serialize(nsACString& aText) const;

  nsresult Deserialize(const nsACString& aText);

  ClientUsageArray Clone() const {
    ClientUsageArray res;
    res.Assign(*this);
    return res;
  }
};

class OriginInfo final {
  friend class GroupInfo;
  friend class QuotaManager;
  friend class QuotaObject;

 public:
  OriginInfo(GroupInfo* aGroupInfo, const nsACString& aOrigin,
             const ClientUsageArray& aClientUsages, uint64_t aUsage,
             int64_t aAccessTime, bool aPersisted, bool aDirectoryExists);

  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(OriginInfo)

  GroupInfo* GetGroupInfo() const { return mGroupInfo; }

  const nsCString& Origin() const { return mOrigin; }

  int64_t LockedUsage() const {
    AssertCurrentThreadOwnsQuotaMutex();

#ifdef DEBUG
    QuotaManager* quotaManager = QuotaManager::Get();
    MOZ_ASSERT(quotaManager);

    uint64_t usage = 0;
    for (Client::Type type : quotaManager->AllClientTypes()) {
      AssertNoOverflow(usage, mClientUsages[type].valueOr(0));
      usage += mClientUsages[type].valueOr(0);
    }
    MOZ_ASSERT(mUsage == usage);
#endif

    return mUsage;
  }

  int64_t LockedAccessTime() const {
    AssertCurrentThreadOwnsQuotaMutex();

    return mAccessTime;
  }

  bool LockedPersisted() const {
    AssertCurrentThreadOwnsQuotaMutex();

    return mPersisted;
  }

  OriginMetadata FlattenToOriginMetadata() const;

  nsresult LockedBindToStatement(mozIStorageStatement* aStatement) const;

 private:
  // Private destructor, to discourage deletion outside of Release():
  ~OriginInfo() {
    MOZ_COUNT_DTOR(OriginInfo);

    MOZ_ASSERT(!mQuotaObjects.Count());
  }

  void LockedDecreaseUsage(Client::Type aClientType, int64_t aSize);

  void LockedResetUsageForClient(Client::Type aClientType);

  UsageInfo LockedGetUsageForClient(Client::Type aClientType);

  void LockedUpdateAccessTime(int64_t aAccessTime) {
    AssertCurrentThreadOwnsQuotaMutex();

    mAccessTime = aAccessTime;
    if (!mAccessed) {
      mAccessed = true;
    }
  }

  void LockedPersist();

  nsTHashMap<nsStringHashKey, NotNull<QuotaObject*>> mQuotaObjects;
  ClientUsageArray mClientUsages;
  GroupInfo* mGroupInfo;
  const nsCString mOrigin;
  uint64_t mUsage;
  int64_t mAccessTime;
  bool mAccessed;
  bool mPersisted;
  /**
   * In some special cases like the LocalStorage client where it's possible to
   * create a Quota-using representation but not actually write any data, we
   * want to be able to track quota for an origin without creating its origin
   * directory or the per-client files until they are actually needed to store
   * data. In those cases, the OriginInfo will be created by
   * EnsureQuotaForOrigin and the resulting mDirectoryExists will be false until
   * the origin actually needs to be created. It is possible for mUsage to be
   * greater than zero while mDirectoryExists is false, representing a state
   * where a client like LocalStorage has reserved quota for disk writes, but
   * has not yet flushed the data to disk.
   */
  bool mDirectoryExists;
};

class OriginInfoAccessTimeComparator {
 public:
  bool Equals(const NotNull<RefPtr<const OriginInfo>>& a,
              const NotNull<RefPtr<const OriginInfo>>& b) const {
    return a->LockedAccessTime() == b->LockedAccessTime();
  }

  bool LessThan(const NotNull<RefPtr<const OriginInfo>>& a,
                const NotNull<RefPtr<const OriginInfo>>& b) const {
    return a->LockedAccessTime() < b->LockedAccessTime();
  }
};

class GroupInfo final {
  friend class GroupInfoPair;
  friend class OriginInfo;
  friend class QuotaManager;
  friend class QuotaObject;

 public:
  GroupInfo(GroupInfoPair* aGroupInfoPair, PersistenceType aPersistenceType)
      : mGroupInfoPair(aGroupInfoPair),
        mPersistenceType(aPersistenceType),
        mUsage(0) {
    MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT);

    MOZ_COUNT_CTOR(GroupInfo);
  }

  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GroupInfo)

  PersistenceType GetPersistenceType() const { return mPersistenceType; }

 private:
  // Private destructor, to discourage deletion outside of Release():
  MOZ_COUNTED_DTOR(GroupInfo)

  already_AddRefed<OriginInfo> LockedGetOriginInfo(const nsACString& aOrigin);

  void LockedAddOriginInfo(NotNull<RefPtr<OriginInfo>>&& aOriginInfo);

  void LockedAdjustUsageForRemovedOriginInfo(const OriginInfo& aOriginInfo);

  void LockedRemoveOriginInfo(const nsACString& aOrigin);

  void LockedRemoveOriginInfos();

  bool LockedHasOriginInfos() {
    AssertCurrentThreadOwnsQuotaMutex();

    return !mOriginInfos.IsEmpty();
  }

  nsTArray<NotNull<RefPtr<OriginInfo>>> mOriginInfos;

  GroupInfoPair* mGroupInfoPair;
  PersistenceType mPersistenceType;
  uint64_t mUsage;
};

// XXX Consider a new name for this class, it has other data members now
// (besides two GroupInfo objects).
class GroupInfoPair {
 public:
  GroupInfoPair(const nsACString& aSuffix, const nsACString& aGroup)
      : mSuffix(aSuffix), mGroup(aGroup) {
    MOZ_COUNT_CTOR(GroupInfoPair);
  }

  MOZ_COUNTED_DTOR(GroupInfoPair)

  const nsCString& Suffix() const { return mSuffix; }

  const nsCString& Group() const { return mGroup; }

  RefPtr<GroupInfo> LockedGetGroupInfo(PersistenceType aPersistenceType) {
    AssertCurrentThreadOwnsQuotaMutex();
    MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT);

    return GetGroupInfoForPersistenceType(aPersistenceType);
  }

  void LockedSetGroupInfo(PersistenceType aPersistenceType,
                          GroupInfo* aGroupInfo) {
    AssertCurrentThreadOwnsQuotaMutex();
    MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT);

    RefPtr<GroupInfo>& groupInfo =
        GetGroupInfoForPersistenceType(aPersistenceType);
    groupInfo = aGroupInfo;
  }

  void LockedClearGroupInfo(PersistenceType aPersistenceType) {
    AssertCurrentThreadOwnsQuotaMutex();
    MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT);

    RefPtr<GroupInfo>& groupInfo =
        GetGroupInfoForPersistenceType(aPersistenceType);
    groupInfo = nullptr;
  }

  bool LockedHasGroupInfos() {
    AssertCurrentThreadOwnsQuotaMutex();

    return mTemporaryStorageGroupInfo || mDefaultStorageGroupInfo;
  }

 private:
  RefPtr<GroupInfo>& GetGroupInfoForPersistenceType(
      PersistenceType aPersistenceType);

  const nsCString mSuffix;
  const nsCString mGroup;
  RefPtr<GroupInfo> mTemporaryStorageGroupInfo;
  RefPtr<GroupInfo> mDefaultStorageGroupInfo;
};

namespace {

class CollectOriginsHelper final : public Runnable {
  uint64_t mMinSizeToBeFreed;

  Mutex& mMutex;
  CondVar mCondVar;

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

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

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

 private:
  ~CollectOriginsHelper() = default;

  NS_IMETHOD
  Run() override;
};

class OriginOperationBase : public BackgroundThreadObject, public Runnable {
 protected:
  nsresult mResultCode;

  enum State {
    // Not yet run.
    State_Initial,

    // Running quota manager initialization on the owning thread.
    State_CreatingQuotaManager,

    // Running on the owning thread in the listener for OpenDirectory.
    State_DirectoryOpenPending,

    // Running on the IO thread.
    State_DirectoryWorkOpen,

    // Running on the owning thread after all work is done.
    State_UnblockingOpen,

    // All done.
    State_Complete
  };

 private:
  State mState;
  bool mActorDestroyed;

 protected:
  bool mNeedsQuotaManagerInit;
  bool mNeedsStorageInit;

 public:
  void NoteActorDestroyed() {
    AssertIsOnOwningThread();

    mActorDestroyed = true;
  }

  bool IsActorDestroyed() const {
    AssertIsOnOwningThread();

    return mActorDestroyed;
  }

 protected:
  explicit OriginOperationBase(
      nsIEventTarget* aOwningThread = GetCurrentEventTarget())
      : BackgroundThreadObject(aOwningThread),
        Runnable("dom::quota::OriginOperationBase"),
        mResultCode(NS_OK),
        mState(State_Initial),
        mActorDestroyed(false),
        mNeedsQuotaManagerInit(false),
        mNeedsStorageInit(false) {}

  // Reference counted.
  virtual ~OriginOperationBase() {
    MOZ_ASSERT(mState == State_Complete);
    MOZ_ASSERT(mActorDestroyed);
  }

#ifdef DEBUG
  State GetState() const { return mState; }
#endif

  void SetState(State aState) {
    MOZ_ASSERT(mState == State_Initial);
    mState = aState;
  }

  void AdvanceState() {
    switch (mState) {
      case State_Initial:
        mState = State_CreatingQuotaManager;
        return;
      case State_CreatingQuotaManager:
        mState = State_DirectoryOpenPending;
        return;
      case State_DirectoryOpenPending:
        mState = State_DirectoryWorkOpen;
        return;
      case State_DirectoryWorkOpen:
        mState = State_UnblockingOpen;
        return;
      case State_UnblockingOpen:
        mState = State_Complete;
        return;
      default:
        MOZ_CRASH("Bad state!");
    }
  }

  NS_IMETHOD
  Run() override;

  virtual void Open() = 0;

  nsresult DirectoryOpen();

  virtual nsresult DoDirectoryWork(QuotaManager& aQuotaManager) = 0;

  void Finish(nsresult aResult);

  virtual void UnblockOpen() = 0;

 private:
  nsresult Init();

  nsresult FinishInit();

  nsresult QuotaManagerOpen();

  nsresult DirectoryWork();
};

class FinalizeOriginEvictionOp : public OriginOperationBase {
  nsTArray<RefPtr<OriginDirectoryLock>> mLocks;

 public:
  FinalizeOriginEvictionOp(nsIEventTarget* aBackgroundThread,
                           nsTArray<RefPtr<OriginDirectoryLock>>&& aLocks)
      : OriginOperationBase(aBackgroundThread), mLocks(std::move(aLocks)) {
    MOZ_ASSERT(!NS_IsMainThread());
  }

  void Dispatch();

  void RunOnIOThreadImmediately();

 private:
  ~FinalizeOriginEvictionOp() = default;

  virtual void Open() override;

  virtual nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override;

  virtual void UnblockOpen() override;
};

class NormalOriginOperationBase
    : public OriginOperationBase,
      public OpenDirectoryListener,
      public SupportsCheckedUnsafePtr<CheckIf<DiagnosticAssertEnabled>> {
  RefPtr<DirectoryLock> mDirectoryLock;

 protected:
  Nullable<PersistenceType> mPersistenceType;
  OriginScope mOriginScope;
  Nullable<Client::Type> mClientType;
  mozilla::Atomic<bool> mCanceled;
  const bool mExclusive;
  bool mNeedsDirectoryLocking;

 public:
  void RunImmediately() {
    MOZ_ASSERT(GetState() == State_Initial);

    MOZ_ALWAYS_SUCCEEDS(this->Run());
  }

 protected:
  NormalOriginOperationBase(const Nullable<PersistenceType>& aPersistenceType,
                            const OriginScope& aOriginScope, bool aExclusive)
      : mPersistenceType(aPersistenceType),
        mOriginScope(aOriginScope),
        mExclusive(aExclusive),
        mNeedsDirectoryLocking(true) {
    AssertIsOnOwningThread();
  }

  ~NormalOriginOperationBase() = default;

 private:
  // Need to declare refcounting unconditionally, because
  // OpenDirectoryListener has pure-virtual refcounting.
  NS_DECL_ISUPPORTS_INHERITED

  virtual void Open() override;

  virtual void UnblockOpen() override;

  // OpenDirectoryListener overrides.
  virtual void DirectoryLockAcquired(DirectoryLock* aLock) override;

  virtual void DirectoryLockFailed() override;

  // Used to send results before unblocking open.
  virtual void SendResults() = 0;
};

class SaveOriginAccessTimeOp : public NormalOriginOperationBase {
  int64_t mTimestamp;

 public:
  SaveOriginAccessTimeOp(PersistenceType aPersistenceType,
                         const nsACString& aOrigin, int64_t aTimestamp)
      : NormalOriginOperationBase(Nullable<PersistenceType>(aPersistenceType),
                                  OriginScope::FromOrigin(aOrigin),
                                  /* aExclusive */ false),
        mTimestamp(aTimestamp) {
    AssertIsOnOwningThread();
  }

 private:
  ~SaveOriginAccessTimeOp() = default;

  virtual nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override;

  virtual void SendResults() override;
};

/*******************************************************************************
 * Actor class declarations
 ******************************************************************************/

class Quota final : public PQuotaParent {
#ifdef DEBUG
  bool mActorDestroyed;
#endif

 public:
  Quota();

  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(mozilla::dom::quota::Quota)

 private:
  ~Quota();

  void StartIdleMaintenance();

  bool VerifyRequestParams(const UsageRequestParams& aParams) const;

  bool VerifyRequestParams(const RequestParams& aParams) const;

  // IPDL methods.
  virtual void ActorDestroy(ActorDestroyReason aWhy) override;

  virtual PQuotaUsageRequestParent* AllocPQuotaUsageRequestParent(
      const UsageRequestParams& aParams) override;

  virtual mozilla::ipc::IPCResult RecvPQuotaUsageRequestConstructor(
      PQuotaUsageRequestParent* aActor,
      const UsageRequestParams& aParams) override;

  virtual bool DeallocPQuotaUsageRequestParent(
      PQuotaUsageRequestParent* aActor) override;

  virtual PQuotaRequestParent* AllocPQuotaRequestParent(
      const RequestParams& aParams) override;

  virtual mozilla::ipc::IPCResult RecvPQuotaRequestConstructor(
      PQuotaRequestParent* aActor, const RequestParams& aParams) override;

  virtual bool DeallocPQuotaRequestParent(PQuotaRequestParent* aActor) override;

  virtual mozilla::ipc::IPCResult RecvStartIdleMaintenance() override;

  virtual mozilla::ipc::IPCResult RecvStopIdleMaintenance() override;

  virtual mozilla::ipc::IPCResult RecvAbortOperationsForProcess(
      const ContentParentId& aContentParentId) override;
};

class QuotaUsageRequestBase : public NormalOriginOperationBase,
                              public PQuotaUsageRequestParent {
 public:
  // May be overridden by subclasses if they need to perform work on the
  // background thread before being run.
  virtual void Init(Quota& aQuota);

 protected:
  QuotaUsageRequestBase()
      : NormalOriginOperationBase(Nullable<PersistenceType>(),
                                  OriginScope::FromNull(),
                                  /* aExclusive */ false) {}

  mozilla::Result<UsageInfo, nsresult> GetUsageForOrigin(
      QuotaManager& aQuotaManager, PersistenceType aPersistenceType,
      const OriginMetadata& aOriginMetadata);

  // Subclasses use this override to set the IPDL response value.
  virtual void GetResponse(UsageRequestResponse& aResponse) = 0;

 private:
  mozilla::Result<UsageInfo, nsresult> GetUsageForOriginEntries(
      QuotaManager& aQuotaManager, PersistenceType aPersistenceType,
      const OriginMetadata& aOriginMetadata, nsIFile& aDirectory,
      bool aInitialized);

  void SendResults() override;

  // IPDL methods.
  void ActorDestroy(ActorDestroyReason aWhy) override;

  mozilla::ipc::IPCResult RecvCancel() final;
};

// A mix-in class to simplify operations that need to process every origin in
// one or more repositories. Sub-classes should call TraverseRepository in their
// DoDirectoryWork and implement a ProcessOrigin method for their per-origin
// logic.
class TraverseRepositoryHelper {
 public:
  TraverseRepositoryHelper() = default;

 protected:
  virtual ~TraverseRepositoryHelper() = default;

  // If ProcessOrigin returns an error, TraverseRepository will immediately
  // terminate and return the received error code to its caller.
  nsresult TraverseRepository(QuotaManager& aQuotaManager,
                              PersistenceType aPersistenceType);

 private:
  virtual const Atomic<bool>& GetIsCanceledFlag() = 0;

  virtual nsresult ProcessOrigin(QuotaManager& aQuotaManager,
                                 nsIFile& aOriginDir, const bool aPersistent,
                                 const PersistenceType aPersistenceType) = 0;
};

class GetUsageOp final : public QuotaUsageRequestBase,
                         public TraverseRepositoryHelper {
  nsTArray<OriginUsage> mOriginUsages;
  nsTHashMap<nsCStringHashKey, uint32_t> mOriginUsagesIndex;

  bool mGetAll;

 public:
  explicit GetUsageOp(const UsageRequestParams& aParams);

 private:
  ~GetUsageOp() = default;

  void ProcessOriginInternal(QuotaManager* aQuotaManager,
                             const PersistenceType aPersistenceType,
                             const nsACString& aOrigin,
                             const int64_t aTimestamp, const bool aPersisted,
                             const uint64_t aUsage);

  nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override;

  const Atomic<bool>& GetIsCanceledFlag() override;

  nsresult ProcessOrigin(QuotaManager& aQuotaManager, nsIFile& aOriginDir,
                         const bool aPersistent,
                         const PersistenceType aPersistenceType) override;

  void GetResponse(UsageRequestResponse& aResponse) override;
};

class GetOriginUsageOp final : public QuotaUsageRequestBase {
  nsCString mSuffix;
  nsCString mGroup;
  uint64_t mUsage;
  uint64_t mFileUsage;
  bool mFromMemory;

 public:
  explicit GetOriginUsageOp(const UsageRequestParams& aParams);

 private:
  ~GetOriginUsageOp() = default;

  virtual nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override;

  void GetResponse(UsageRequestResponse& aResponse) override;
};

class QuotaRequestBase : public NormalOriginOperationBase,
                         public PQuotaRequestParent {
 public:
  // May be overridden by subclasses if they need to perform work on the
  // background thread before being run.
  virtual void Init(Quota& aQuota);

 protected:
  explicit QuotaRequestBase(bool aExclusive)
      : NormalOriginOperationBase(Nullable<PersistenceType>(),
                                  OriginScope::FromNull(), aExclusive) {}

  // Subclasses use this override to set the IPDL response value.
  virtual void GetResponse(RequestResponse& aResponse) = 0;

 private:
  virtual void SendResults() override;

  // IPDL methods.
  virtual void ActorDestroy(ActorDestroyReason aWhy) override;
};

class StorageNameOp final : public QuotaRequestBase {
  nsString mName;

 public:
  StorageNameOp();

  void Init(Quota& aQuota) override;

 private:
  ~StorageNameOp() = default;

  nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override;

  void GetResponse(RequestResponse& aResponse) override;
};

class InitializedRequestBase : public QuotaRequestBase {
 protected:
  bool mInitialized;

 public:
  void Init(Quota& aQuota) override;

 protected:
  InitializedRequestBase();
};

class StorageInitializedOp final : public InitializedRequestBase {
 private:
  ~StorageInitializedOp() = default;

  nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override;

  void GetResponse(RequestResponse& aResponse) override;
};

class TemporaryStorageInitializedOp final : public InitializedRequestBase {
 private:
  ~TemporaryStorageInitializedOp() = default;

  nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override;

  void GetResponse(RequestResponse& aResponse) override;
};

class InitOp final : public QuotaRequestBase {
 public:
  InitOp();

  void Init(Quota& aQuota) override;

 private:
  ~InitOp() = default;

  nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override;

  void GetResponse(RequestResponse& aResponse) override;
};

class InitTemporaryStorageOp final : public QuotaRequestBase {
 public:
  InitTemporaryStorageOp();

  void Init(Quota& aQuota) override;

 private:
  ~InitTemporaryStorageOp() = default;

  nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override;

  void GetResponse(RequestResponse& aResponse) override;
};

class InitializeOriginRequestBase : public QuotaRequestBase {
 protected:
  nsCString mSuffix;
  nsCString mGroup;
  bool mCreated;

 public:
  void Init(Quota& aQuota) override;

 protected:
  InitializeOriginRequestBase(PersistenceType aPersistenceType,
                              const PrincipalInfo& aPrincipalInfo);
};

class InitializePersistentOriginOp final : public InitializeOriginRequestBase {
 public:
  explicit InitializePersistentOriginOp(const RequestParams& aParams);

 private:
  ~InitializePersistentOriginOp() = default;

  nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override;

  void GetResponse(RequestResponse& aResponse) override;
};

class InitializeTemporaryOriginOp final : public InitializeOriginRequestBase {
 public:
  explicit InitializeTemporaryOriginOp(const RequestParams& aParams);

 private:
  ~InitializeTemporaryOriginOp() = default;

  nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override;

  void GetResponse(RequestResponse& aResponse) override;
};

class ResetOrClearOp final : public QuotaRequestBase {
  const bool mClear;

 public:
  explicit ResetOrClearOp(bool aClear);

  void Init(Quota& aQuota) override;

 private:
  ~ResetOrClearOp() = default;

  void DeleteFiles(QuotaManager& aQuotaManager);

  void DeleteStorageFile(QuotaManager& aQuotaManager);

  virtual nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override;

  virtual void GetResponse(RequestResponse& aResponse) override;
};

class ClearRequestBase : public QuotaRequestBase {
 protected:
  explicit ClearRequestBase(bool aExclusive) : QuotaRequestBase(aExclusive) {
    AssertIsOnOwningThread();
  }

  void DeleteFiles(QuotaManager& aQuotaManager,
                   PersistenceType aPersistenceType);

  nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override;
};

class ClearOriginOp final : public ClearRequestBase {
  const ClearResetOriginParams mParams;
  const bool mMatchAll;

 public:
  explicit ClearOriginOp(const RequestParams& aParams);

  void Init(Quota& aQuota) override;

 private:
  ~ClearOriginOp() = default;

  void GetResponse(RequestResponse& aResponse) override;
};

class ClearDataOp final : public ClearRequestBase {
  const ClearDataParams mParams;

 public:
  explicit ClearDataOp(const RequestParams& aParams);

  void Init(Quota& aQuota) override;

 private:
  ~ClearDataOp() = default;

  void GetResponse(RequestResponse& aResponse) override;
};

class ResetOriginOp final : public QuotaRequestBase {
 public:
  explicit ResetOriginOp(const RequestParams& aParams);

  void Init(Quota& aQuota) override;

 private:
  ~ResetOriginOp() = default;

  nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override;

  void GetResponse(RequestResponse& aResponse) override;
};

class PersistRequestBase : public QuotaRequestBase {
  const PrincipalInfo mPrincipalInfo;

 protected:
  nsCString mSuffix;
  nsCString mGroup;

 public:
  void Init(Quota& aQuota) override;

 protected:
  explicit PersistRequestBase(const PrincipalInfo& aPrincipalInfo);
};

class PersistedOp final : public PersistRequestBase {
  bool mPersisted;

 public:
  explicit PersistedOp(const RequestParams& aParams);

 private:
  ~PersistedOp() = default;

  nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override;

  void GetResponse(RequestResponse& aResponse) override;
};

class PersistOp final : public PersistRequestBase {
 public:
  explicit PersistOp(const RequestParams& aParams);

 private:
  ~PersistOp() = default;

  nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override;

  void GetResponse(RequestResponse& aResponse) override;
};

class EstimateOp final : public QuotaRequestBase {
  nsCString mGroup;
  uint64_t mUsage;
  uint64_t mLimit;

 public:
  explicit EstimateOp(const RequestParams& aParams);

 private:
  ~EstimateOp() = default;

  virtual nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override;

  void GetResponse(RequestResponse& aResponse) override;
};

class ListOriginsOp final : public QuotaRequestBase,
                            public TraverseRepositoryHelper {
  // XXX Bug 1521541 will make each origin has it's own state.
  nsTArray<nsCString> mOrigins;

 public:
  ListOriginsOp();

  void Init(Quota& aQuota) override;

 private:
  ~ListOriginsOp() = default;

  nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override;

  const Atomic<bool>& GetIsCanceledFlag() override;

  nsresult ProcessOrigin(QuotaManager& aQuotaManager, nsIFile& aOriginDir,
                         const bool aPersistent,
                         const PersistenceType aPersistenceType) override;

  void GetResponse(RequestResponse& aResponse) override;
};

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

class StoragePressureRunnable final : public Runnable {
  const uint64_t mUsage;

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

 private:
  ~StoragePressureRunnable() = default;

  NS_DECL_NSIRUNNABLE
};

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

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

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

  void Start();

  void End();

 private:
  ~RecordQuotaInfoLoadTimeHelper() = default;

  NS_DECL_NSIRUNNABLE
};

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

#ifdef QM_PRINCIPALINFO_VERIFICATION_ENABLED

class PrincipalVerifier final : public Runnable {
  nsTArray<PrincipalInfo> mPrincipalInfos;

 public:
  static already_AddRefed<PrincipalVerifier> CreateAndDispatch(
      nsTArray<PrincipalInfo>&& aPrincipalInfos);

 private:
  explicit PrincipalVerifier(nsTArray<PrincipalInfo>&& aPrincipalInfos)
      : Runnable("dom::quota::PrincipalVerifier"),
        mPrincipalInfos(std::move(aPrincipalInfos)) {
    AssertIsOnIOThread();
  }

  virtual ~PrincipalVerifier() = default;

  Result<Ok, nsCString> CheckPrincipalInfoValidity(
      const PrincipalInfo& aPrincipalInfo);

  NS_DECL_NSIRUNNABLE
};

#endif

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

template <typename T, bool = std::is_unsigned_v<T>>
struct IntChecker {
  static void Assert(T aInt) {
    static_assert(std::is_integral_v<T>, "Not an integer!");
    MOZ_ASSERT(aInt >= 0);
  }
};

template <typename T>
struct IntChecker<T, true> {
  static void Assert(T aInt) {
    static_assert(std::is_integral_v<T>, "Not an integer!");
  }
};

template <typename T>
void AssertNoOverflow(uint64_t aDest, T aArg) {
  IntChecker<T>::Assert(aDest);
  IntChecker<T>::Assert(aArg);
  MOZ_ASSERT(UINT64_MAX - aDest >= uint64_t(aArg));
}

template <typename T, typename U>
void AssertNoUnderflow(T aDest, U aArg) {
  IntChecker<T>::Assert(aDest);
  IntChecker<T>::Assert(aArg);
  MOZ_ASSERT(uint64_t(aDest) >= uint64_t(aArg));
}

inline bool IsDotFile(const nsAString& aFileName) {
  return QuotaManager::IsDotFile(aFileName);
}

inline bool IsOSMetadata(const nsAString& aFileName) {
  return QuotaManager::IsOSMetadata(aFileName);
}

bool IsOriginMetadata(const nsAString& aFileName) {
  return aFileName.EqualsLiteral(METADATA_FILE_NAME) ||
         aFileName.EqualsLiteral(METADATA_V2_FILE_NAME) ||
         IsOSMetadata(aFileName);
}

bool IsTempMetadata(const nsAString& aFileName) {
  return aFileName.EqualsLiteral(METADATA_TMP_FILE_NAME) ||
         aFileName.EqualsLiteral(METADATA_V2_TMP_FILE_NAME);
}

// Return whether the group was actually updated.
Result<bool, nsresult> MaybeUpdateGroupForOrigin(
    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 {
    OriginAttributes originAttributes;
    nsCString originNoSuffix;
    QM_TRY(OkIf(originAttributes.PopulateFromOrigin(aOriginMetadata.mOrigin,
                                                    originNoSuffix)),
           Err(NS_ERROR_FAILURE));

    RefPtr<MozURL> url;
    QM_TRY(MozURL::Init(getter_AddRefs(url), originNoSuffix), QM_PROPAGATE,
           [&originNoSuffix](const nsresult) {
             QM_WARNING("A URL %s is not recognized by MozURL",
                        originNoSuffix.get());
           });

    QM_TRY_INSPECT(const auto& baseDomain,
                   MOZ_TO_RESULT_INVOKE_TYPED(nsAutoCString, *url, BaseDomain));

    const nsCString upToDateGroup = baseDomain + aOriginMetadata.mSuffix;

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

#ifdef QM_PRINCIPALINFO_VERIFICATION_ENABLED
      ContentPrincipalInfo contentPrincipalInfo;
      contentPrincipalInfo.attrs() = originAttributes;
      contentPrincipalInfo.originNoSuffix() = originNoSuffix;
      contentPrincipalInfo.spec() = originNoSuffix;
      contentPrincipalInfo.baseDomain() = baseDomain;

      PrincipalInfo principalInfo(contentPrincipalInfo);

      nsTArray<PrincipalInfo> principalInfos;
      principalInfos.AppendElement(principalInfo);

      RefPtr<PrincipalVerifier> principalVerifier =
          PrincipalVerifier::CreateAndDispatch(std::move(principalInfos));
#endif
    }
  }

  return updated;
}

}  // namespace

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

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

#ifdef DEBUG

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

#endif  // DEBUG

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

bool IsOnIOThread() {
  QuotaManager* quotaManager = QuotaManager::Get();
  NS_ASSERTION(quotaManager, "Must have a manager here!");

  bool currentThread;
  return NS_SUCCEEDED(
             quotaManager->IOThread()->IsOnCurrentThread(&currentThread)) &&
         currentThread;
}

void AssertIsOnIOThread() {
  NS_ASSERTION(IsOnIOThread(), "Running on the wrong thread!");
}

void DiagnosticAssertIsOnIOThread() { MOZ_DIAGNOSTIC_ASSERT(IsOnIOThread()); }

void AssertCurrentThreadOwnsQuotaMutex() {
#ifdef DEBUG
  QuotaManager* quotaManager = QuotaManager::Get();
  NS_ASSERTION(quotaManager, "Must have a manager here!");

  quotaManager->AssertCurrentThreadOwnsQuotaMutex();
#endif
}

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

  nsContentUtils::LogSimpleConsoleError(
      NS_ConvertUTF8toUTF16(
          nsPrintfCString("Quota %s: %s:%" PRIu32, aStr, aFile, aLine)),
      "quota", 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;

typedef nsTArray<CheckedUnsafePtr<NormalOriginOperationBase>>
    NormalOriginOpArray;
StaticAutoPtr<NormalOriginOpArray> gNormalOriginOps;

// Constants for temporary storage limit computing.
static const uint32_t kDefaultChunkSizeKB = 10 * 1024;

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

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

  gNormalOriginOps->AppendElement(&aNormalOriginOp);
}

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

  gNormalOriginOps->RemoveElement(&aNormalOriginOp);

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

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

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

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

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

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

  nsTArray<OriginProps> mOriginProps;

  nsCOMPtr<nsIFile> mDirectory;

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

  NS_INLINE_DECL_REFCOUNTING(StorageOperationBase)

 protected:
  virtual ~StorageOperationBase() = default;

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

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

  int64_t GetOriginLastModifiedTime(const OriginProps& aOriginProps);

  nsresult RemoveObsoleteOrigin(const OriginProps& aOriginProps);

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

  nsresult ProcessOriginDirectories();

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

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

 private:
  using Tokenizer =
      nsCCharSeparatedTokenizerTemplate<NS_TokenizerIgnoreNothing>;

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

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

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

  const nsCString mOrigin;
  Tokenizer mTokenizer;

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

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

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

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

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

  ResultType Parse(nsACString& aSpec);

 private:
  void HandleScheme(const nsDependentCSubstring& aToken);

  void HandlePathnameComponent(const nsDependentCSubstring& aToken);

  void HandleToken(const nsDependentCSubstring& aToken);

  void HandleTrailingSeparator();
};

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

  nsresult ProcessRepository();

 protected:
  virtual ~RepositoryOperationBase() = default;

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

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

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

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

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

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

  LazyInitializedOnce<const LegacyPersistenceType> mLegacyPersistenceType;

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

  nsresult Init();

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

  PersistenceType PersistenceTypeFromLegacyPersistentSpec(
      const nsCString& aSpec);

  PersistenceType PersistenceTypeFromSpec(const nsCString& aSpec) override;

  nsresult MaybeUpgradeOriginDirectory(nsIFile* aDirectory);

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

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

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

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

  nsresult Init();

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

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

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

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

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

 private:
  nsresult MaybeRemoveMorgueDirectory(const OriginProps& aOriginProps);

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

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

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

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

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

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

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

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

  nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) override;

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

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

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

  nsresult Init();

  nsresult RestoreMetadata2File();

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

auto MakeSanitizedOriginCString(const nsACString& aOrigin) {
#ifdef XP_WIN
  NS_ASSERTION(!strcmp(QuotaManager::kReplaceChars,
                       FILE_ILLEGAL_CHARACTERS FILE_PATH_SEPARATOR),
               "Illegal file characters have changed!");
#endif

  nsAutoCString res{aOrigin};

  res.ReplaceChar(QuotaManager::kReplaceChars, '+');

  return res;
}

auto MakeSanitizedOriginString(const nsACString& aOrigin) {
  // An origin string is ASCII-only, since it is obtained via
  // nsIPrincipal::GetOrigin, which returns an ACString.
  return NS_ConvertASCIItoUTF16(MakeSanitizedOriginCString(aOrigin));
}

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

  QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_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(
                                       GetLastModifiedTime(file, aTimestamp));

                                   return Ok{};
                                 }));
          break;

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

          // Bug 1595445 will handle unknown files here.

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

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

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

          if (timestamp > *aTimestamp) {
            *aTimestamp = timestamp;
          }
          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();
  }

  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(aDirectory, Create,
                                          nsIFile::DIRECTORY_TYPE, 0755)
                         .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(aDirectory, IsDirectory));
    QM_TRY(OkIf(isDirectory), Err(NS_ERROR_UNEXPECTED));
  }

  return !exists;
}

enum FileFlag { Truncate, Update, Append };

Result<nsCOMPtr<nsIOutputStream>, nsresult> GetOutputStream(
    nsIFile& aFile, FileFlag aFileFlag) {
  AssertIsOnIOThread();

  switch (aFileFlag) {
    case FileFlag::Truncate:
      QM_TRY_RETURN(NS_NewLocalFileOutputStream(&aFile));

    case FileFlag::Update: {
      QM_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE(&aFile, Exists));

      if (!exists) {
        return nsCOMPtr<nsIOutputStream>();
      }

      QM_TRY_INSPECT(const auto& stream, NS_NewLocalFileStream(&aFile));

      nsCOMPtr<nsIOutputStream> outputStream = do_QueryInterface(stream);
      QM_TRY(OkIf(outputStream), Err(NS_ERROR_FAILURE));

      return outputStream;
    }

    case FileFlag::Append:
      QM_TRY_RETURN(NS_NewLocalFileOutputStream(
          &aFile, PR_WRONLY | PR_CREATE_FILE | PR_APPEND));

    default:
      MOZ_CRASH("Should never get here!");
  }
}

Result<nsCOMPtr<nsIBinaryOutputStream>, nsresult> GetBinaryOutputStream(
    nsIFile& aFile, FileFlag aFileFlag) {
  QM_TRY_UNWRAP(auto outputStream, GetOutputStream(aFile, aFileFlag));

  QM_TRY(OkIf(outputStream), Err(NS_ERROR_UNEXPECTED));

  return nsCOMPtr<nsIBinaryOutputStream>(
      NS_NewObjectOutputStream(outputStream));
}

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

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

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

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

  OriginAttributes groupAttributes;

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

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

  nsCString group = groupPrefix + groupNoSuffix;

  OriginAttributes originAttributes;

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

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

  nsCString origin = originPrefix + originNoSuffix;

  MOZ_ASSERT(groupPrefix == originPrefix);

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

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

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

  QM_TRY(stream->Write64(aTimestamp));

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

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

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

  QM_TRY(stream->Flush());

  QM_TRY(stream->Close());

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

  return NS_OK;
}

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

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

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

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

  QM_TRY(stream->Write64(aTimestamp));

  QM_TRY(stream->WriteBoolean(aPersisted));

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

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

  // The suffix isn't used right now, but we might need it in future. It's
  // a bit of redundancy we can live with given how painful is to upgrade
  // metadata files.
  QM_TRY(stream->WriteStringZ(aOriginMetadata.mSuffix.get()));

  QM_TRY(stream->WriteStringZ(aOriginMetadata.mGroup.get()));

  QM_TRY(stream->WriteStringZ(aOriginMetadata.mOrigin.get()));

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

  QM_TRY(stream->Flush());

  QM_TRY(stream->Close());

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

  return NS_OK;
}

Result<nsCOMPtr<nsIBinaryInputStream>, nsresult> GetBinaryInputStream(
    nsIFile& aDirectory, const nsAString& aFilename) {
  MOZ_ASSERT(!NS_IsMainThread());

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

  QM_TRY(file->Append(aFilename));

  QM_TRY_UNWRAP(auto stream, NS_NewLocalFileInputStream(file));

  QM_TRY_INSPECT(const auto& bufferedStream,
                 NS_NewBufferedInputStream(stream.forget(), 512));

  QM_TRY(OkIf(bufferedStream), Err(NS_ERROR_FAILURE));

  return nsCOMPtr<nsIBinaryInputStream>(
      NS_NewObjectInputStream(bufferedStream));
}

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

  uint64_t availableSpaceKB = aAvailableSpaceBytes / 1024;

  // Prevent division by zero below.
  uint32_t chunkSizeKB;
  if (StaticPrefs::dom_quotaManager_temporaryStorage_chunkSize()) {
    chunkSizeKB = StaticPrefs::dom_quotaManager_temporaryStorage_chunkSize();
  } else {
    chunkSizeKB = kDefaultChunkSizeKB;
  }

  // Grow/shrink in chunkSizeKB units, deliberately, so that in the common case
  // we don't shrink temporary storage and evict origin data every time we
  // initialize.
  availableSpaceKB = (availableSpaceKB / chunkSizeKB) * chunkSizeKB;

  // Allow temporary storage to consume up to half the available space.
  return availableSpaceKB * .50 * 1024;
}

}  // namespace

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

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

#ifdef QM_SCOPED_LOG_EXTRA_INFO_ENABLED
  ScopedLogExtraInfo::Initialize();
#endif

  if (!QuotaManager::IsRunningGTests()) {
    // This service has to be started on the main thread currently.
    const nsCOMPtr<mozIStorageService> ss =
        do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID);

    QM_WARNONLY_TRY(OkIf(ss));
  }

  QM_WARNONLY_TRY(QuotaManager::Initialize());

#ifdef DEBUG
  gQuotaManagerInitialized = true;
#endif
}

PQuotaParent* AllocPQuotaParent() {
  AssertIsOnBackgroundThread();

  if (NS_WARN_IF(QuotaManager::IsShuttingDown())) {
    return nullptr;
  }

  auto actor = MakeRefPtr<Quota>();

  return actor.forget().take();
}

bool DeallocPQuotaParent(PQuotaParent* aActor) {
  AssertIsOnBackgroundThread();
  MOZ_ASSERT(aActor);

  RefPtr<Quota> actor = dont_AddRef(static_cast<Quota*>(aActor));
  return true;
}

bool RecvShutdownQuotaManager() {
  AssertIsOnBackgroundThread();

  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
void QuotaManager::Observer::ShutdownCompleted() {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(sInstance);

  sInstance->mShutdownComplete = true;
}

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

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

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

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

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

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

  return NS_OK;
}

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

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

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

  sInstance = nullptr;

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

  return NS_OK;
}

NS_IMPL_ISUPPORTS(QuotaManager::Observer, nsIObserver)

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

  nsresult rv;

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

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

    gBasePath = new nsString();

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

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

    gStorageName = new nsString();

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

    gBuildId = new nsCString();

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

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

    MaybeEnableNextGenLocalStorage();

    return NS_OK;
  }

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

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

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

    mShutdownComplete = false;

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

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

    MOZ_ALWAYS_TRUE(SpinEventLoopUntil([&]() { return mShutdownComplete; }));

    gBasePath = nullptr;

    gStorageName = nullptr;

    gBuildId = nullptr;

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

    return NS_OK;
  }

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

    return NS_OK;
  }

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

    return NS_OK;
  }

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

/*******************************************************************************
 * Quota object
 ******************************************************************************/

void QuotaObject::AddRef() {
  QuotaManager* quotaManager = QuotaManager::Get();
  if (!quotaManager) {
    NS_ERROR("Null quota manager, this shouldn't happen, possible leak!");

    ++mRefCnt;

    return;
  }

  MutexAutoLock lock(quotaManager->mQuotaMutex);

  ++mRefCnt;
}

void QuotaObject::Release() {
  QuotaManager* quotaManager = QuotaManager::Get();
  if (!quotaManager) {
    NS_ERROR("Null quota manager, this shouldn't happen, possible leak!");

    nsrefcnt count = --mRefCnt;
    if (count == 0) {
      mRefCnt = 1;
      delete this;
    }

    return;
  }

  {
    MutexAutoLock lock(quotaManager->mQuotaMutex);

    --mRefCnt;

    if (mRefCnt > 0) {
      return;
    }

    if (mOriginInfo) {
      mOriginInfo->mQuotaObjects.Remove(mPath);
    }
  }

  delete this;
}

bool QuotaObject::MaybeUpdateSize(int64_t aSize, bool aTruncate) {
  QuotaManager* quotaManager = QuotaManager::Get();
  MOZ_ASSERT(quotaManager);

  MutexAutoLock lock(quotaManager->mQuotaMutex);

  return LockedMaybeUpdateSize(aSize, aTruncate);
}

bool QuotaObject::IncreaseSize(int64_t aDelta) {
  MOZ_ASSERT(aDelta >= 0);

  QuotaManager* quotaManager = QuotaManager::Get();
  MOZ_ASSERT(quotaManager);

  MutexAutoLock lock(quotaManager->mQuotaMutex);

  AssertNoOverflow(mSize, aDelta);
  int64_t size = mSize + aDelta;

  return LockedMaybeUpdateSize(size, /* aTruncate */ false);
}

void QuotaObject::DisableQuotaCheck() {
  QuotaManager* quotaManager = QuotaManager::Get();
  MOZ_ASSERT(quotaManager);

  MutexAutoLock lock(quotaManager->mQuotaMutex);

  mQuotaCheckDisabled = true;
}

void QuotaObject::EnableQuotaCheck() {
  QuotaManager* quotaManager = QuotaManager::Get();
  MOZ_ASSERT(quotaManager);

  MutexAutoLock lock(quotaManager->mQuotaMutex);

  mQuotaCheckDisabled = false;
}

bool QuotaObject::LockedMaybeUpdateSize(int64_t aSize, bool aTruncate) {
  QuotaManager* quotaManager = QuotaManager::Get();
  MOZ_ASSERT(quotaManager);

  quotaManager->mQuotaMutex.AssertCurrentThreadOwns();

  if (mWritingDone == false && mOriginInfo) {
    mWritingDone = true;
    StorageActivityService::SendActivity(mOriginInfo->mOrigin);
  }

  if (mQuotaCheckDisabled) {
    return true;
  }

  if (mSize == aSize) {
    return true;
  }

  if (!mOriginInfo) {
    mSize = aSize;
    return true;
  }

  GroupInfo* groupInfo = mOriginInfo->mGroupInfo;
  MOZ_ASSERT(groupInfo);

  if (mSize > aSize) {
    if (aTruncate) {
      const int64_t delta = mSize - aSize;

      AssertNoUnderflow(quotaManager->mTemporaryStorageUsage, delta);
      quotaManager->mTemporaryStorageUsage -= delta;

      if (!mOriginInfo->LockedPersisted()) {
        AssertNoUnderflow(groupInfo->mUsage, delta);
        groupInfo->mUsage -= delta;
      }

      AssertNoUnderflow(mOriginInfo->mUsage, delta);
      mOriginInfo->mUsage -= delta;

      MOZ_ASSERT(mOriginInfo->mClientUsages[mClientType].isSome());
      AssertNoUnderflow(mOriginInfo->mClientUsages[mClientType].value(), delta);
      mOriginInfo->mClientUsages[mClientType] =
          Some(mOriginInfo->mClientUsages[mClientType].value() - delta);

      mSize = aSize;
    }
    return true;
  }

  MOZ_ASSERT(mSize < aSize);

  RefPtr<GroupInfo> complementaryGroupInfo =
      groupInfo->mGroupInfoPair->LockedGetGroupInfo(
          ComplementaryPersistenceType(groupInfo->mPersistenceType));

  uint64_t delta = aSize - mSize;

  AssertNoOverflow(mOriginInfo->mClientUsages[mClientType].valueOr(0), delta);
  uint64_t newClientUsage =
      mOriginInfo->mClientUsages[mClientType].valueOr(0) + delta;

  AssertNoOverflow(mOriginInfo->mUsage, delta);
  uint64_t newUsage = mOriginInfo->mUsage + delta;

  // Temporary storage has no limit for origin usage (there's a group and the
  // global limit though).

  uint64_t newGroupUsage = groupInfo->mUsage;
  if (!mOriginInfo->LockedPersisted()) {
    AssertNoOverflow(groupInfo->mUsage, delta);
    newGroupUsage += delta;

    uint64_t groupUsage = groupInfo->mUsage;
    if (complementaryGroupInfo) {
      AssertNoOverflow(groupUsage, complementaryGroupInfo->mUsage);
      groupUsage += complementaryGroupInfo->mUsage;
    }

    // Temporary storage has a hard limit for group usage (20 % of the global
    // limit).
    AssertNoOverflow(groupUsage, delta);
    if (groupUsage + delta > quotaManager->GetGroupLimit()) {
      return false;
    }
  }

  AssertNoOverflow(quotaManager->mTemporaryStorageUsage, delta);
  uint64_t newTemporaryStorageUsage =
      quotaManager->mTemporaryStorageUsage + delta;

  if (newTemporaryStorageUsage > quotaManager->mTemporaryStorageLimit) {
    // This will block the thread without holding the lock while waitting.

    AutoTArray<RefPtr<OriginDirectoryLock>, 10> locks;
    uint64_t sizeToBeFreed;

    if (IsOnBackgroundThread()) {
      MutexAutoUnlock autoUnlock(quotaManager->mQuotaMutex);

      sizeToBeFreed = quotaManager->CollectOriginsForEviction(delta, locks);
    } else {
      sizeToBeFreed =
          quotaManager->LockedCollectOriginsForEviction(delta, locks);
    }

    if (!sizeToBeFreed) {
      uint64_t usage = quotaManager->mTemporaryStorageUsage;

      MutexAutoUnlock autoUnlock(quotaManager->mQuotaMutex);

      quotaManager->NotifyStoragePressure(usage);

      return false;
    }

    NS_ASSERTION(sizeToBeFreed >= delta, "Huh?");

    {
      MutexAutoUnlock autoUnlock(quotaManager->mQuotaMutex);

      for (const auto& lock : locks) {
        quotaManager->DeleteFilesForOrigin(lock->GetPersistenceType(),
                                           lock->Origin());
      }
    }

    // Relocked.

    NS_ASSERTION(mOriginInfo, "How come?!");

    for (const auto& lock : locks) {
      MOZ_ASSERT(!(lock->GetPersistenceType() == groupInfo->mPersistenceType &&
                   lock->Origin() == mOriginInfo->mOrigin),
                 "Deleted itself!");

      quotaManager->LockedRemoveQuotaForOrigin(lock->GetPersistenceType(),
                                               lock->OriginMetadata());
    }

    // We unlocked and relocked several times so we need to recompute all the
    // essential variables and recheck the group limit.

    AssertNoUnderflow(aSize, mSize);
    delta = aSize - mSize;

    AssertNoOverflow(mOriginInfo->mClientUsages[mClientType].valueOr(0), delta);
    newClientUsage = mOriginInfo->mClientUsages[mClientType].valueOr(0) + delta;

    AssertNoOverflow(mOriginInfo->mUsage, delta);
    newUsage = mOriginInfo->mUsage + delta;

    newGroupUsage = groupInfo->mUsage;
    if (!mOriginInfo->LockedPersisted()) {
      AssertNoOverflow(groupInfo->mUsage, delta);
      newGroupUsage += delta;

      uint64_t groupUsage = groupInfo->mUsage;
      if (complementaryGroupInfo) {
        AssertNoOverflow(groupUsage, complementaryGroupInfo->mUsage);
        groupUsage += complementaryGroupInfo->mUsage;
      }

      AssertNoOverflow(groupUsage, delta);
      if (groupUsage + delta > quotaManager->GetGroupLimit()) {
        // Unfortunately some other thread increased the group usage in the
        // meantime and we are not below the group limit anymore.

        // However, the origin eviction must be finalized in this case too.
        MutexAutoUnlock autoUnlock(quotaManager->mQuotaMutex);

        quotaManager->FinalizeOriginEviction(std::move(locks));

        return false;
      }
    }

    AssertNoOverflow(quotaManager->mTemporaryStorageUsage, delta);
    newTemporaryStorageUsage = quotaManager->mTemporaryStorageUsage + delta;

    NS_ASSERTION(
        newTemporaryStorageUsage <= quotaManager->mTemporaryStorageLimit,
        "How come?!");

    // Ok, we successfully freed enough space and the operation can continue
    // without throwing the quota error.
    mOriginInfo->mClientUsages[mClientType] = Some(newClientUsage);

    mOriginInfo->mUsage = newUsage;
    if (!mOriginInfo->LockedPersisted()) {
      groupInfo->mUsage = newGroupUsage;
    }
    quotaManager->mTemporaryStorageUsage = newTemporaryStorageUsage;
    ;

    // Some other thread could increase the size in the meantime, but no more
    // than this one.
    MOZ_ASSERT(mSize < aSize);
    mSize = aSize;

    // Finally, release IO thread only objects and allow next synchronized
    // ops for the evicted origins.
    MutexAutoUnlock autoUnlock(quotaManager->mQuotaMutex);

    quotaManager->FinalizeOriginEviction(std::move(locks));

    return true;
  }

  mOriginInfo->mClientUsages[mClientType] = Some(newClientUsage);

  mOriginInfo->mUsage = newUsage;
  if (!mOriginInfo->LockedPersisted()) {
    groupInfo->mUsage = newGroupUsage;
  }
  quotaManager->mTemporaryStorageUsage = newTemporaryStorageUsage;

  mSize = aSize;

  return true;
}

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

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

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

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

  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(instance->Init());

  gInstance = instance;

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

void QuotaManager::GetOrCreate(nsIRunnable* aCallback) {
  AssertIsOnBackgroundThread();

  Unused << GetOrCreate();

  MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(aCallback));
}

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

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

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

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

    gInstance = nullptr;
  }

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

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

// static
bool QuotaManager::IsOSMetadata(const nsAString& aFileName) {
  return aFileName.EqualsLiteral(DSSTORE_FILE_NAME) ||
         aFileName.EqualsLiteral(DESKTOP_FILE_NAME) ||
         aFileName.LowerCaseEqualsLiteral(DESKTOP_INI_FILE_NAME) ||
         aFileName.LowerCaseEqualsLiteral(THUMBS_DB_FILE_NAME);
}

// static
bool QuotaManager::IsDotFile(const nsAString& aFileName) {
  return aFileName.First() == char16_t('.');
}

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;
        }

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

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

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

  // Split locks into separate arrays and filter out locks for persistent
  // storage, they can't block us.
  const auto [temporaryStorageLocks, defaultStorageLocks] = [this] {
    nsTArray<NotNull<const DirectoryLockImpl*>> temporaryStorageLocks;
    nsTArray<NotNull<const DirectoryLockImpl*>> defaultStorageLocks;
    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 {
        MOZ_ASSERT(persistenceType.Value() == PERSISTENCE_TYPE_PERSISTENT);

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

    return std::pair(std::move(temporaryStorageLocks),
                     std::move(defaultStorageLocks));
  }();

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

  const auto [inactiveOrigins, sizeToBeFreed] =
      [this, &temporaryStorageLocks = temporaryStorageLocks,
       &defaultStorageLocks = defaultStorageLocks, 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);
          }
        }

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

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

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

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

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

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

      lock->AcquireImmediately();

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

    return sizeToBeFreed;
  }

  return 0;
}

template <typename P>
void QuotaManager::CollectPendingOriginsForListing(P aPredicate) {
  MutexAutoLock lock(mQuotaMutex);

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

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

    RefPtr<GroupInfo> groupInfo =
        pair->LockedGetGroupInfo(PERSISTENCE_TYPE_DEFAULT);
    if (groupInfo) {
      for (const auto& originInfo : groupInfo->mOriginInfos) {
        if (!originInfo->mDirectoryExists) {
          aPredicate(originInfo);
        }
      }
    }
  }
}

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

#ifdef XP_WIN
  CacheUseDOSDevicePathSyntaxPrefValue();
#endif

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

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

  QM_TRY(baseDir->Append(mStorageName));

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

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

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

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

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

  // Make a timer here to avoid potential failures later. We don't actually
  // initialize the timer until shutdown.
  nsCOMPtr shutdownTimer = NS_NewTimer();
  QM_TRY(OkIf(shutdownTimer), Err(NS_ERROR_FAILURE));

  mShutdownTimer.init(WrapNotNullUnchecked(std::move(shutdownTimer)));

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

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

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

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

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

  return NS_OK;
}

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

  if (auto* const quotaManager = QuotaManager::Get()) {
    quotaManager->MaybeRecordShutdownStep(Some(aClientType), aStepDescription);
  }
}

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

  MaybeRecordShutdownStep(Nothing{}, aStepDescription);
}

void QuotaManager::MaybeRecordShutdownStep(
    const Maybe<Client::Type> aClientType, const nsACString& aStepDescription) {
  if (!mShutdownStarted) {
    // We are not shutting down yet, we intentionally ignore this here to avoid
    // that every caller has to make a distinction for shutdown vs. non-shutdown
    // situations.
    return;
  }

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

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

  if (aClientType) {
    AssertIsOnBackgroundThread();

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

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

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

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

void QuotaManager::Shutdown() {
  AssertIsOnOwningThread();
  MOZ_ASSERT(!mShutdownStarted);

  // Setting this flag prevents the service from being recreated and prevents
  // further storagess from being created.
  if (gShutdown.exchange(true)) {
    NS_ERROR("Shutdown more than once?!");
  }

  StopIdleMaintenance();

  mShutdownStartedAt.init(TimeStamp::NowLoRes());
  mShutdownStarted = true;

  const auto& allClientTypes = AllClientTypes();

  bool needsToWait = false;
  for (Client::Type type : allClientTypes) {
    needsToWait |= (*mClients)[type]->InitiateShutdownWorkThreads();
  }
  needsToWait |= static_cast<bool>(gNormalOriginOps);

  // If any clients cannot shutdown immediately, spin the event loop while we
  // wait on all the threads to close. Our timer may fire during that loop.
  if (needsToWait) {
    MOZ_ALWAYS_SUCCEEDS(
        (*mShutdownTimer)
            ->InitWithNamedFuncCallback(
                [](nsITimer* aTimer, void* aClosure) {
                  auto* const quotaManager =
                      static_cast<QuotaManager*>(aClosure);

                  for (Client::Type type : quotaManager->AllClientTypes()) {
                    // XXX This is a workaround to unblock shutdown, which ought
                    // to be removed by Bug 1682326.
                    if (type == Client::IDB) {
                      (*quotaManager->mClients)[type]->AbortAllOperations();
                    }

                    (*quotaManager->mClients)[type]->ForceKillActors();
                  }

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

                        nsCString annotation;

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

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

                          if (gNormalOriginOps) {
                            MutexAutoLock lock(quotaManager->mQuotaMutex);

                            annotation.AppendPrintf(
                                "QM: %zu normal origin ops "
                                "pending\nIntermediate "
                                "steps:\n%s\n",
                                gNormalOriginOps->Length(),
                                quotaManager->mQuotaManagerShutdownSteps.get());
                          }
                        }

                        // We expect that at least one quota client didn't
                        // complete its shutdown.
                        MOZ_DIAGNOSTIC_ASSERT(!annotation.IsEmpty());

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

                        MOZ_CRASH("Quota manager shutdown timed out");
                      },
                      aClosure, SHUTDOWN_FORCE_CRASH_TIMEOUT_MS,
                      nsITimer::TYPE_ONE_SHOT,
                      "quota::QuotaManager::ForceCrashTimer"));
                },
                this, SHUTDOWN_FORCE_KILL_TIMEOUT_MS, nsITimer::TYPE_ONE_SHOT,
                "quota::QuotaManager::ForceKillTimer"));

    MOZ_ALWAYS_TRUE(SpinEventLoopUntil([this, &allClientTypes] {
      return !gNormalOriginOps &&
             std::all_of(allClientTypes.cbegin(), allClientTypes.cend(),
                         [&self = *this](const auto type) {
                           return (*self.mClients)[type]->IsShutdownCompleted();
                         });
    }));
  }

  for (Client::Type type : allClientTypes) {
    (*mClients)[type]->FinalizeShutdownWorkThreads();
  }

  // Cancel the timer regardless of whether it actually fired.
  QM_WARNONLY_TRY((*mShutdownTimer)->Cancel());

  // NB: It's very important that runnable is destroyed on this thread
  // (i.e. after we join the IO thread) because we can't release the
  // QuotaManager on the IO thread. This should probably use
  // NewNonOwningRunnableMethod ...
  RefPtr<Runnable> runnable =
      NewRunnableMethod("dom::quota::QuotaManager::ShutdownStorage", this,
                        &QuotaManager::ShutdownStorage);
  MOZ_ASSERT(runnable);

  // Give clients a chance to cleanup IO thread only objects.
  QM_WARNONLY_TRY((*mIOThread)->Dispatch(runnable, NS_DISPATCH_NORMAL));

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

  for (RefPtr<DirectoryLockImpl>& lock : mPendingDirectoryLocks) {
    lock->Invalidate();
  }
}

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, 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, 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, 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(!IsShuttingDown());

  MutexAutoLock lock(mQuotaMutex);

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

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

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

    MutexAutoUnlock autoUnlock(mQuotaMutex);

    auto op = MakeRefPtr<SaveOriginAccessTimeOp>(
        aPersistenceType, aOriginMetadata.mOrigin, timestamp);

    RegisterNormalOriginOp(*op);

    op->RunImmediately();
  }
}

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

  MutexAutoLock lock(mQuotaMutex);

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

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

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

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

  mGroupInfoPairs.Clear();

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

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

  auto recordQuotaInfoLoadTimeHelper =
      MakeRefPtr<RecordQuotaInfoLoadTimeHelper>();
  recordQuotaInfoLoadTimeHelper->Start();

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

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

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

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

          FullOriginMetadata fullOriginMetadata;

          fullOriginMetadata.mPersistenceType = maybePersistenceType.value();

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

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

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

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

          Unused << updated;

          // We don't need to update the .metadata-v2 file on disk here,
          // EnsureTemporaryOriginIsInitialized is responsible for doing that.
          // We just need to use correct group before initializing quota for the
          // given origin. (Note that calling 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.)

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

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

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

          if (accessed) {
            QM_TRY_INSPECT(
                const auto& directory,
                GetDirectoryForOrigin(fullOriginMetadata.mPersistenceType,
                                      fullOriginMetadata.mOrigin));

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

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

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

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

            // Calling 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(InitializeOrigin(fullOriginMetadata.mPersistenceType,
                                    fullOriginMetadata,
                                    fullOriginMetadata.mLastAccessTime,
                                    fullOriginMetadata.mPersisted, directory));
          } else {
            InitQuotaForOrigin(fullOriginMetadata, clientUsages, usage);
          }

          return Ok{};
        }));

    autoRemoveQuota.release();

    return NS_OK;
  };

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

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

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

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

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

            return buildId == *gBuildId;
          }
        }

        return false;
      }()));

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

  if (!loadQuotaFromCache ||
      !StaticPrefs::dom_quotaManager_loadQuotaFromCache() ||
      ![&LoadQuotaFromCache] {
        QM_WARNONLY_TRY_UNWRAP(auto res, ToResult(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 : kBestEffortPersistenceTypes) {
      if (NS_WARN_IF(IsShuttingDown())) {
        RETURN_STATUS_OR_RESULT(statusKeeper, NS_ERROR_ABORT);
      }

      QM_TRY(([&]() -> Result<Ok, nsresult> {
        QM_TRY(([this, type] {
                 const nsresult rv = InitializeRepository(type);
                 mInitializationInfo.MaybeRecordFirstInitializationAttempt(
                     type == PERSISTENCE_TYPE_DEFAULT
                         ? Initialization::DefaultRepository
                         : Initialization::TemporaryRepository,
                     rv);
                 return rv;
               }()),
               OK_IN_NIGHTLY_PROPAGATE_IN_OTHERS, statusKeeperFunc);

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

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

  recordQuotaInfoLoadTimeHelper->End();

  autoRemoveQuota.release();

  return NS_OK;
}

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

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

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

  QM_TRY(transaction.Start(), QM_VOID);

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

  nsCOMPtr<mozIStorageStatement> insertStmt;

  {
    MutexAutoLock lock(mQuotaMutex);

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

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

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

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

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

          if (insertStmt) {
            MOZ_ALWAYS_SUCCEEDS(insertStmt->Reset());
          } else {
            QM_TRY_UNWRAP(
                insertStmt,
                MOZ_TO_RESULT_INVOKE_TYPED(
                    nsCOMPtr<mozIStorageStatement>, mStorageConnection,
                    CreateStatement,
                    "INSERT INTO origin (repository_id, 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(originInfo->LockedBindToStatement(insertStmt), QM_VOID);

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

        groupInfo->LockedRemoveOriginInfos();
      }

      iter.Remove();
    }
  }

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

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

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

  if (aFileSizeOut) {
    *aFileSizeOut = 0;
  }

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

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

#ifdef DEBUG
  {
    QM_TRY_INSPECT(
        const auto& directory,
        GetDirectoryForOrigin(aPersistenceType, aOriginMetadata.mOrigin),
        nullptr);

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

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

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

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

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

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

                     return 0;
                   }

                   return aFileSize;
                 }()),
                 nullptr);

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

    GroupInfoPair* pair;
    if (!mGroupInfoPairs.Get(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<QuotaObject*> quotaObject =
        originInfo->mQuotaObjects.LookupOrInsertWith(path, [&] {
          // Create a new QuotaObject. The hashtable is not responsible to
          // delete the QuotaObject.
          return WrapNotNullUnchecked(
              new QuotaObject(originInfo, aClientType, path, fileSize));
        });

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

  if (aFileSizeOut) {
    *aFileSizeOut = fileSize;
  }

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

already_AddRefed<QuotaObject> QuotaManager::GetQuotaObject(
    PersistenceType aPersistenceType, const 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::GetDirectoryForOrigin(
    PersistenceType aPersistenceType, const nsACString& aASCIIOrigin) const {
  QM_TRY_UNWRAP(auto directory,
                QM_NewLocalFile(GetStoragePath(aPersistenceType)));

  QM_TRY(directory->Append(MakeSanitizedOriginString(aASCIIOrigin)));

  return directory;
}

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

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

  QM_TRY(helper->Init());

  QM_TRY(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(binaryStream, Read64));

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

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

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

  fullOriginMetadata.mPersistenceType = aPersistenceType;

  QM_TRY_UNWRAP(
      fullOriginMetadata.mSuffix,
      MOZ_TO_RESULT_INVOKE_TYPED(nsCString, binaryStream, ReadCString));

  QM_TRY_UNWRAP(
      fullOriginMetadata.mGroup,
      MOZ_TO_RESULT_INVOKE_TYPED(nsCString, binaryStream, ReadCString));

  QM_TRY_UNWRAP(
      fullOriginMetadata.mOrigin,
      MOZ_TO_RESULT_INVOKE_TYPED(nsCString, binaryStream, ReadCString));

  // Currently unused (used to be isApp).
  QM_TRY_INSPECT(const bool& dummy,
                 MOZ_TO_RESULT_INVOKE(binaryStream, ReadBoolean));
  Unused << dummy;

  QM_TRY(binaryStream->Close());

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

  if (updated) {
    // Only overwriting .metadata-v2 (used to overwrite .metadata too) to reduce
    // I/O.
    QM_TRY(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(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(RestoreDirectoryMetadata2(aDirectory));

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

nsresult QuotaManager::InitializeRepository(PersistenceType aPersistenceType) {
  MOZ_ASSERT(aPersistenceType == PERSISTENCE_TYPE_TEMPORARY ||
             aPersistenceType == PERSISTENCE_TYPE_DEFAULT);

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

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

  Unused << created;

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

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

  struct RenameAndInitInfo {
    nsCOMPtr<nsIFile> mOriginDirectory;
    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]() -> Result<Ok, nsresult> {
                    QM_TRY_INSPECT(
                        const auto& leafName,
                        MOZ_TO_RESULT_INVOKE_TYPED(nsAutoString, childDirectory,
                                                   GetLeafName));

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

                    switch (dirEntryKind) {
                      case nsIFileKind::ExistsAsDirectory: {
                        QM_TRY_UNWRAP(
                            auto metadata,
                            LoadFullOriginMetadataWithRestore(childDirectory));

                        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.
                            ToResult(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(childDirectory->Remove(true));

                              return Ok{};
                            })));

                        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 (const auto& info : renameAndInitInfos) {
    QM_TRY(([&]() -> Result<Ok, nsresult> {
      QM_TRY(([&directory, &info, this,
               aPersistenceType]() -> 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(targetDirectory, Exists));

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

                 return Ok{};
               }

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

               QM_TRY(InitializeOrigin(
                   aPersistenceType, info.mFullOriginMetadata, info.mTimestamp,
                   info.mPersisted, targetDirectory));

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

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

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

  return NS_OK;
}

nsresult QuotaManager::InitializeOrigin(PersistenceType aPersistenceType,
                                        const 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_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((*mClients)[clientType]
                                     ->InitOriginWithoutTracking(
                                         aPersistenceType, aOriginMetadata,
                                         /* aCanceled */ Atomic<bool>(false)));
                        }

                        break;
                      }

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

                        if (IsTempMetadata(leafName)) {
                          QM_TRY(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);

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

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

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

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

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

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

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

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

      return NS_OK;
    }

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

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

    return NS_OK;
  }();

  mInitializationInfo.MaybeRecordFirstInitializationAttempt(
      Initialization::UpgradeFromIndexedDBDirectory, rv);

  return rv;
}

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

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

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

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

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

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

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

        return NS_OK;
      }
    }

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

      QM_TRY(helper->Init());

      QM_TRY(helper->ProcessRepository());

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

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

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

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

        helper = MakeRefPtr<CreateOrUpgradeDirectoryMetadataHelper>(
            temporaryStorageDir);

        QM_TRY(helper->Init());

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

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

    return NS_OK;
  }();

  mInitializationInfo.MaybeRecordFirstInitializationAttempt(
      Initialization::UpgradeFromPersistentStorageDirectory, rv);

  return rv;
}

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

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

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

    if (!exists) {
      continue;
    }

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

    QM_TRY(helper->Init());

    QM_TRY(helper->ProcessRepository());
  }

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

    MOZ_ASSERT(storageVersion == aOldVersion);
  }
#endif

  QM_TRY(aConnection->SetSchemaVersion(aNewVersion));

  return NS_OK;
}

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

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

    return NS_OK;
  }();

  mInitializationInfo.MaybeRecordFirstInitializationAttempt(
      Initialization::UpgradeStorageFrom0_0To1_0, rv);

  return rv;
}

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

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

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

    return NS_OK;
  }();

  mInitializationInfo.MaybeRecordFirstInitializationAttempt(
      Initialization::UpgradeStorageFrom1_0To2_0, rv);

  return rv;
}

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

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

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

    return NS_OK;
  }();

  mInitializationInfo.MaybeRecordFirstInitializationAttempt(
      Initialization::UpgradeStorageFrom2_0To2_1, rv);

  return rv;
}

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

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

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

    return NS_OK;
  }();

  mInitializationInfo.MaybeRecordFirstInitializationAttempt(
      Initialization::UpgradeStorageFrom2_1To2_2, rv);

  return rv;
}

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

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

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

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

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

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

    return NS_OK;
  }();

  mInitializationInfo.MaybeRecordFirstInitializationAttempt(
      Initialization::UpgradeStorageFrom2_2To2_3, rv);

  return rv;
}

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

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

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

  QM_TRY(MaybeRemoveLocalStorageDirectories());

  InvalidateQuotaCache();

  // Finally remove the ls archive, so we don't have to check all origin
  // directories next time this method is called.
  QM_TRY(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(defaultStorageDir, Exists));

  if (!exists) {
    return NS_OK;
  }

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

        QM_TRY_INSPECT(const 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(lsDir, Exists));

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

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

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

                return Ok{};
              }
            }

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

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

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

            break;
          }

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

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

            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(aLsArchiveFile, Exists));
    MOZ_ASSERT(!exists);
  }
#endif

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

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

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

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

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

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

    QM_TRY(stmt->Finalize());

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

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

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

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

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

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

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

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

    // Finally, rename ls-archive-tmp.sqlite to ls-archive.sqlite
    QM_TRY(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_TYPED(nsCOMPtr<mozIStorageConnection>, ss,
                                 OpenUnsharedDatabase, &aLsArchiveFile));

  QM_TRY(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(aLsArchiveFile, Exists));
    MOZ_ASSERT(exists);
  }
#endif

  QM_TRY_INSPECT(const bool& isDirectory,
                 MOZ_TO_RESULT_INVOKE(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, ToResultGet<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_TYPED(
                                     nsCOMPtr<mozIStorageConnection>, ss,
                                     OpenUnsharedDatabase, &aLsArchiveFile));

  // 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(
      ToResult(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(MaybeRemoveLocalStorageDirectories());

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

    MOZ_ASSERT(exists);
  }
#endif

  QM_TRY(aLsArchiveFile.Remove(false));

  QM_TRY(CopyLocalStorageArchiveFromWebAppsStore(aLsArchiveFile));

  QM_TRY_UNWRAP(auto connection,
                CreateLocalStorageArchiveConnection(aLsArchiveFile));

  QM_TRY(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(
      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(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(aStorageFile, Exists));

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

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

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

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

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

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

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

  return NS_OK;
}

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

  QM_TRY_UNWRAP(auto storageVersion,
                MOZ_TO_RESULT_INVOKE(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(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(storageDir, Exists));

    const bool newDirectory = !storageDirExists;

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

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

    QM_TRY(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(CreateTables(&aConnection));

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

      QM_TRY(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(UpgradeStorageFrom0_0To1_0(&aConnection));
        } else if (storageVersion == MakeStorageVersion(1, 0)) {
          QM_TRY(UpgradeStorageFrom1_0To2_0(&aConnection));
        } else if (storageVersion == MakeStorageVersion(2, 0)) {
          QM_TRY(UpgradeStorageFrom2_0To2_1(&aConnection));
        } else if (storageVersion == MakeStorageVersion(2, 1)) {
          QM_TRY(UpgradeStorageFrom2_1To2_2(&aConnection));
        } else if (storageVersion == MakeStorageVersion(2, 2)) {
          QM_TRY(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(aConnection, GetSchemaVersion));
      }

      MOZ_ASSERT(storageVersion == kStorageVersion);
    }

    QM_TRY(transaction.Commit());
  }

  return NS_OK;
}

Result<Ok, QMResult> QuotaManager::MaybeRemoveLocalStorageArchiveTmpFile() {
  AssertIsOnIOThread();

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

  QM_TRY_INSPECT(
      const bool& exists,
      MOZ_TO_RESULT_INVOKE(lsArchiveTmpFile, Exists).mapErr(ToQMResult));

  if (exists) {
    QM_TRY(ToQMResult(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(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(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(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(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(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(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(aLsArchiveFile, Exists));

  // If it exists, remove it. It might be a directory, so remove it recursively.
  if (exists) {
    QM_TRY(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, ToResultGet<nsCOMPtr<mozIStorageService>>(
                                     MOZ_SELECT_OVERLOAD(do_GetService),
                                     MOZ_STORAGE_SERVICE_CONTRACTID));

  QM_TRY_UNWRAP(
      const auto connection,
      MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr<mozIStorageConnection>, ss,
                                 OpenUnsharedDatabase, &aLsArchiveFile));

  QM_TRY(StorageDBUpdater::CreateCurrentSchema(connection));

  QM_TRY(InitializeLocalStorageArchive(connection));

  QM_TRY(
      SaveLocalStorageArchiveVersion(connection, kLocalStorageArchiveVersion));

  return Ok{};
}

nsresult QuotaManager::EnsureStorageIsInitialized() {
  DiagnosticAssertIsOnIOThread();

  const auto firstInitializationAttempt =
      mInitializationInfo.FirstInitializationAttempt(Initialization::Storage);

  if (mStorageConnection) {
    MOZ_ASSERT(firstInitializationAttempt.Recorded());
    return NS_OK;
  }

  auto rv = [&firstInitializationAttempt, this]() -> nsresult {
    const auto maybeExtraInfo =
        firstInitializationAttempt.Pending()
            ? Some(ScopedLogExtraInfo{
                  ScopedLogExtraInfo::kTagContext,
                  "dom::quota::FirstInitializationAttempt::Storage"_ns})
            : Nothing{};

    QM_TRY_INSPECT(const auto& storageFile, QM_NewLocalFile(mBasePath));
    QM_TRY(storageFile->Append(mStorageName + kSQLiteSuffix));

    QM_TRY(MaybeUpgradeToDefaultStorageDirectory(*storageFile));

    QM_TRY_INSPECT(const auto& ss, ToResultGet<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_TYPED(nsCOMPtr<mozIStorageConnection>, ss,
                                       OpenUnsharedDatabase, storageFile),
            // Predicate.
            IsDatabaseCorruptionError,
            // Fallback.
            ErrToDefaultOk<nsCOMPtr<mozIStorageConnection>>));

    if (!connection) {
      // Nuke the database file.
      QM_TRY(storageFile->Remove(false));

      QM_TRY_UNWRAP(connection, MOZ_TO_RESULT_INVOKE_TYPED(
                                    nsCOMPtr<mozIStorageConnection>, ss,
                                    OpenUnsharedDatabase, storageFile));
    }

    // We want extra durability for this important file.
    QM_TRY(connection->ExecuteSimpleSQL("PRAGMA synchronous = EXTRA;"_ns));

    // Check to make sure that the storage version is correct.
    QM_TRY(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(MaybeRemoveLocalStorageDataAndArchive(*lsArchiveFile));
    }

    QM_TRY_UNWRAP(mCacheUsable, MaybeCreateOrUpgradeCache(*connection));

    if (mCacheUsable && gInvalidateQuotaCache) {
      QM_TRY(InvalidateCache(*connection));

      gInvalidateQuotaCache = false;
    }

    mStorageConnection = std::move(connection);

    return NS_OK;
  }();

  firstInitializationAttempt.MaybeRecord(rv);

  return rv;
}

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);

  auto& originInitializationInfo =
      mInitializationInfo.MutableOriginInitializationInfoRef(
          aOriginMetadata.mOrigin);

  const auto firstInitializationAttempt =
      originInitializationInfo.FirstInitializationAttempt(
          OriginInitialization::PersistentOrigin);

  auto res = [&firstInitializationAttempt, &aOriginMetadata, this]()
      -> mozilla::Result<std::pair<nsCOMPtr<nsIFile>, bool>, nsresult> {
    const auto maybeExtraInfo =
        firstInitializationAttempt.Pending()
            ? Some(ScopedLogExtraInfo{
                  ScopedLogExtraInfo::kTagContext,
                  "dom::quota::FirstOriginInitializationAttempt::PersistentOrigin"_ns})
            : Nothing{};

    QM_TRY_UNWRAP(auto directory,
                  GetDirectoryForOrigin(PERSISTENCE_TYPE_PERSISTENT,
                                        aOriginMetadata.mOrigin));

    if (mInitializedOrigins.Contains(aOriginMetadata.mOrigin)) {
      return std::pair(std::move(directory), false);
    }

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

    QM_TRY_INSPECT(const int64_t& timestamp,
                   ([this, created, &directory,
                     &aOriginMetadata]() -> Result<int64_t, nsresult> {
                     if (created) {
                       const int64_t timestamp = PR_Now();

                       // Only creating .metadata-v2 to reduce IO.
                       QM_TRY(CreateDirectoryMetadata2(*directory, timestamp,
                                                       /* aPersisted */ true,
                                                       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(InitializeOrigin(PERSISTENCE_TYPE_PERSISTENT, aOriginMetadata,
                            timestamp,
                            /* aPersisted */ true, directory));

    mInitializedOrigins.AppendElement(aOriginMetadata.mOrigin);

    return std::pair(std::move(directory), created);
  }();

  firstInitializationAttempt.MaybeRecord(res.isOk() ? NS_OK : res.inspectErr());

  return res;
}

Result<std::pair<nsCOMPtr<nsIFile>, bool>, nsresult>
QuotaManager::EnsureTemporaryOriginIsInitialized(
    PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata) {
  AssertIsOnIOThread();
  MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT);
  MOZ_DIAGNOSTIC_ASSERT(mStorageConnection);
  MOZ_DIAGNOSTIC_ASSERT(mTemporaryStorageInitialized);

  auto& originInitializationInfo =
      mInitializationInfo.MutableOriginInitializationInfoRef(
          aOriginMetadata.mOrigin);

  const auto firstInitializationAttempt =
      originInitializationInfo.FirstInitializationAttempt(
          OriginInitialization::TemporaryOrigin);

  auto res = [&firstInitializationAttempt, &aPersistenceType, &aOriginMetadata,
              this]()
      -> mozilla::Result<std::pair<nsCOMPtr<nsIFile>, bool>, nsresult> {
    const auto maybeExtraInfo =
        firstInitializationAttempt.Pending()
            ? Some(ScopedLogExtraInfo{
                  ScopedLogExtraInfo::kTagContext,
                  "dom::quota::FirstOriginInitializationAttempt::TemporaryOrigin"_ns})
            : Nothing{};

    // Get directory for this origin and persistence type.
    QM_TRY_UNWRAP(
        auto directory,
        GetDirectoryForOrigin(aPersistenceType, aOriginMetadata.mOrigin));

    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(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);
  }();

  firstInitializationAttempt.MaybeRecord(res.isOk() ? NS_OK : res.inspectErr());

  return res;
}

nsresult QuotaManager::EnsureTemporaryStorageIsInitialized() {
  AssertIsOnIOThread();
  MOZ_DIAGNOSTIC_ASSERT(mStorageConnection);

  const auto firstInitializationAttempt =
      mInitializationInfo.FirstInitializationAttempt(
          Initialization::TemporaryStorage);

  if (mTemporaryStorageInitialized) {
    MOZ_ASSERT(firstInitializationAttempt.Recorded());
    return NS_OK;
  }

  auto rv = [&firstInitializationAttempt, this]() -> nsresult {
    const auto maybeExtraInfo =
        firstInitializationAttempt.Pending()
            ? Some(ScopedLogExtraInfo{
                  ScopedLogExtraInfo::kTagContext,
                  "dom::quota::FirstInitializationAttempt::TemporaryStorage"_ns})
            : Nothing{};

    QM_TRY_INSPECT(
        const auto& storageDir,
        ToResultGet<nsCOMPtr<nsIFile>>(MOZ_SELECT_OVERLOAD(do_CreateInstance),
                                       NS_LOCAL_FILE_CONTRACTID));

    QM_TRY(storageDir->InitWithPath(GetStoragePath()));

    // The storage directory must exist before calling GetDiskSpaceAvailable.
    QM_TRY_INSPECT(const bool& created, EnsureDirectory(*storageDir));

    Unused << created;

    // Check for available disk space users have on their device where storage
    // directory lives.
    QM_TRY_INSPECT(const int64_t& diskSpaceAvailable,
                   MOZ_TO_RESULT_INVOKE(storageDir, GetDiskSpaceAvailable));

    MOZ_ASSERT(diskSpaceAvailable >= 0);

    QM_TRY(LoadQuota());

    mTemporaryStorageInitialized = true;

    // Available disk space shouldn't be used directly for temporary storage
    // limit calculation since available disk space is affected by existing data
    // stored in temporary storage. So we need to increase it by the temporary
    // storage size (that has been calculated in LoadQuota) before passing to
    // GetTemporaryStorageLimit..
    mTemporaryStorageLimit = GetTemporaryStorageLimit(
        /* aAvailableSpaceBytes */ diskSpaceAvailable + mTemporaryStorageUsage);

    CleanupTemporaryStorage();

    if (mCacheUsable) {
      QM_TRY(InvalidateCache(*mStorageConnection));
    }

    return NS_OK;
  }();

  firstInitializationAttempt.MaybeRecord(rv);

  return rv;
}

void QuotaManager::ShutdownStorage() {
  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(aDirectory, Exists));

  if (!exists) {
    QM_TRY_INSPECT(const auto& leafName,
                   MOZ_TO_RESULT_INVOKE_TYPED(nsString, aDirectory, GetLeafName)
                       .map([](const auto& leafName) {
                         return NS_ConvertUTF16toUTF8(leafName);
                       }));

    QM_TRY(OkIf(IsSanitizedOriginValid(leafName)), Err(NS_ERROR_FAILURE),
           [](const auto&) {
             QM_WARNING(
                 "Preventing creation of a new origin directory which is not "
                 "supported by our origin parser or is obsolete!");
           });
  }

  QM_TRY_RETURN(EnsureDirectory(aDirectory));
}

nsresult QuotaManager::AboutToClearOrigins(
    const Nullable<PersistenceType>& aPersistenceType,
    const OriginScope& aOriginScope,
    const Nullable<Client::Type>& aClientType) {
  AssertIsOnIOThread();

  if (aClientType.IsNull()) {
    for (Client::Type type : AllClientTypes()) {
      QM_TRY((*mClients)[type]->AboutToClearOrigins(aPersistenceType,
                                                    aOriginScope));
    }
  } else {
    QM_TRY((*mClients)[aClientType.Value()]->AboutToClearOrigins(
        aPersistenceType, aOriginScope));
  }

  return NS_OK;
}

void QuotaManager::OriginClearCompleted(
    PersistenceType aPersistenceType, const nsACString& aOrigin,
    const Nullable<Client::Type>& aClientType) {
  AssertIsOnIOThread();

  if (aClientType.IsNull()) {
    if (aPersistenceType == PERSISTENCE_TYPE_PERSISTENT) {
      mInitializedOrigins.RemoveElement(aOrigin);
    }

    for (Client::Type type : AllClientTypes()) {
      (*mClients)[type]->OnOriginClearCompleted(aPersistenceType, aOrigin);
    }
  } else {
    (*mClients)[aClientType.Value()]->OnOriginClearCompleted(aPersistenceType,
                                                             aOrigin);
  }
}

Client* QuotaManager::GetClient(Client::Type aClientType) {
  MOZ_ASSERT(aClientType >= Client::IDB);
  MOZ_ASSERT(aClientType < Client::TypeMax());

  return (*mClients)[aClientType];
}

const AutoTArray<Client::Type, Client::TYPE_MAX>&
QuotaManager::AllClientTypes() {
  if (CachedNextGenLocalStorageEnabled()) {
    return *mAllClientTypes;
  }
  return *mAllClientTypesExceptLS;
}

uint64_t QuotaManager::GetGroupLimit() const {
  // To avoid one group evicting all the rest, limit the amount any one group
  // can use to 20% resp. a fifth. To prevent individual sites from using
  // exorbitant amounts of storage where there is a lot of free space, cap the
  // group limit to 2GB.
  const uint64_t x = std::min<uint64_t>(mTemporaryStorageLimit / 5, 2 GB);

  // In low-storage situations, make an exception (while not exceeding the total
  // storage limit).
  return std::min<uint64_t>(mTemporaryStorageLimit,
                            std::max<uint64_t>(x, 10 MB));
}

uint64_t QuotaManager::GetGroupUsage(const nsACString& aGroup) {
  AssertIsOnIOThread();

  uint64_t usage = 0;

  {
    MutexAutoLock lock(mQuotaMutex);

    GroupInfoPair* pair;
    if (mGroupInfoPairs.Get(aGroup, &pair)) {
      for (const PersistenceType type : kBestEffortPersistenceTypes) {
        RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(type);
        if (groupInfo) {
          AssertNoOverflow(usage, groupInfo->mUsage);
          usage += groupInfo->mUsage;
        }
      }
    }
  }

  return usage;
}

uint64_t QuotaManager::GetOriginUsage(
    const 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;
}

void QuotaManager::NotifyStoragePressure(uint64_t aUsage) {
  mQuotaMutex.AssertNotCurrentThreadOwns();

  RefPtr<StoragePressureRunnable> storagePressureRunnable =
      new StoragePressureRunnable(aUsage);

  MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(storagePressureRunnable));
}

// static
void QuotaManager::GetStorageId(PersistenceType aPersistenceType,
                                const nsACString& aOrigin,
                                Client::Type aClientType,
                                nsACString& aDatabaseId) {
  nsAutoCString str;
  str.AppendInt(aPersistenceType);
  str.Append('*');
  str.Append(aOrigin);
  str.Append('*');
  str.AppendInt(aClientType);

  aDatabaseId = str;
}

// static
bool QuotaManager::IsPrincipalInfoValid(const PrincipalInfo& aPrincipalInfo) {
  switch (aPrincipalInfo.type()) {
    // A system principal is acceptable.
    case PrincipalInfo::TSystemPrincipalInfo: {
      return true;
    }

    // Validate content principals to ensure that the spec, originNoSuffix and
    // baseDomain are sane.
    case PrincipalInfo::TContentPrincipalInfo: {
      const ContentPrincipalInfo& info =
          aPrincipalInfo.get_ContentPrincipalInfo();

      // Verify the principal spec parses.
      RefPtr<MozURL> specURL;
      nsresult rv = MozURL::Init(getter_AddRefs(specURL), info.spec());
      if (NS_WARN_IF(NS_FAILED(rv))) {
        QM_WARNING("A URL %s is not recognized by MozURL", info.spec().get());
        return false;
      }

      // Verify the principal originNoSuffix matches spec.
      nsCString originNoSuffix;
      specURL->Origin(originNoSuffix);

      if (NS_WARN_IF(originNoSuffix != info.originNoSuffix())) {
        QM_WARNING("originNoSuffix (%s) doesn't match passed one (%s)!",
                   originNoSuffix.get(), info.originNoSuffix().get());
        return false;
      }

      if (NS_WARN_IF(info.originNoSuffix().EqualsLiteral(kChromeOrigin))) {
        return false;
      }

      if (NS_WARN_IF(info.originNoSuffix().FindChar('^', 0) != -1)) {
        QM_WARNING("originNoSuffix (%s) contains the '^' character!",
                   info.originNoSuffix().get());
        return false;
      }

      // Verify the principal baseDomain exists.
      if (NS_WARN_IF(info.baseDomain().IsVoid())) {
        return false;
      }

      // Verify the principal baseDomain matches spec.
      nsCString baseDomain;
      rv = specURL->BaseDomain(baseDomain);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return false;
      }

      if (NS_WARN_IF(baseDomain != info.baseDomain())) {
        QM_WARNING("baseDomain (%s) doesn't match passed one (%s)!",
                   baseDomain.get(), info.baseDomain().get());
        return false;
      }

      return true;
    }

    default: {
      break;
    }
  }

  // Null and expanded principals are not acceptable.
  return false;
}

// static
PrincipalMetadata 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();

      PrincipalMetadata principalMetadata;

      info.attrs().CreateSuffix(principalMetadata.mSuffix);

      principalMetadata.mGroup = info.baseDomain() + principalMetadata.mSuffix;

      principalMetadata.mOrigin =
          info.originNoSuffix() + principalMetadata.mSuffix;

      return principalMetadata;
    }

    default: {
      MOZ_CRASH("Should never get here!");
    }
  }
}

// static
nsAutoCString QuotaManager::GetOriginFromValidatedPrincipalInfo(
    const PrincipalInfo& aPrincipalInfo) {
  MOZ_ASSERT(IsPrincipalInfoValid(aPrincipalInfo));

  switch (aPrincipalInfo.type()) {
    case PrincipalInfo::TSystemPrincipalInfo: {
      return nsAutoCString{GetOriginForChrome()};
    }

    case PrincipalInfo::TContentPrincipalInfo: {
      const ContentPrincipalInfo& info =
          aPrincipalInfo.get_ContentPrincipalInfo();

      nsAutoCString suffix;

      info.attrs().CreateSuffix(suffix);

      return info.originNoSuffix() + suffix;
    }

    default: {
      MOZ_CRASH("Should never get here!");
    }
  }
}

// static
Result<PrincipalMetadata, nsresult>