Source code

Revision control

Copy as Markdown

Other Tools

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "ActorsParent.h"
// Local includes
#include "CanonicalQuotaObject.h"
#include "ClientUsageArray.h"
#include "Flatten.h"
#include "FirstInitializationAttemptsImpl.h"
#include "InitializationUtils.h"
#include "GroupInfo.h"
#include "GroupInfoPair.h"
#include "NormalOriginOperationBase.h"
#include "OriginOperationBase.h"
#include "OriginOperations.h"
#include "OriginParser.h"
#include "OriginScope.h"
#include "OriginInfo.h"
#include "QuotaCommon.h"
#include "QuotaManager.h"
#include "QuotaPrefs.h"
#include "ResolvableNormalOriginOp.h"
#include "SanitizationUtils.h"
#include "ScopedLogExtraInfo.h"
#include "UsageInfo.h"
// Global includes
#include <cinttypes>
#include <cstdlib>
#include <cstring>
#include <algorithm>
#include <cstdint>
#include <functional>
#include <new>
#include <numeric>
#include <tuple>
#include <type_traits>
#include <utility>
#include "DirectoryLockImpl.h"
#include "ErrorList.h"
#include "MainThreadUtils.h"
#include "mozIStorageAsyncConnection.h"
#include "mozIStorageConnection.h"
#include "mozIStorageService.h"
#include "mozIStorageStatement.h"
#include "mozStorageCID.h"
#include "mozStorageHelper.h"
#include "mozilla/AlreadyAddRefed.h"
#include "mozilla/AppShutdown.h"
#include "mozilla/Assertions.h"
#include "mozilla/Atomics.h"
#include "mozilla/Attributes.h"
#include "mozilla/AutoRestore.h"
#include "mozilla/BasePrincipal.h"
#include "mozilla/CheckedInt.h"
#include "mozilla/CondVar.h"
#include "mozilla/InitializedOnce.h"
#include "mozilla/Logging.h"
#include "mozilla/MacroForEach.h"
#include "mozilla/Maybe.h"
#include "mozilla/Mutex.h"
#include "mozilla/NotNull.h"
#include "mozilla/Now.h"
#include "mozilla/OriginAttributes.h"
#include "mozilla/Preferences.h"
#include "mozilla/RefPtr.h"
#include "mozilla/Result.h"
#include "mozilla/ResultExtensions.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/SpinEventLoopUntil.h"
#include "mozilla/StaticPrefs_dom.h"
#include "mozilla/StaticPtr.h"
#include "mozilla/SystemPrincipal.h"
#include "mozilla/TextUtils.h"
#include "mozilla/TimeStamp.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/Unused.h"
#include "mozilla/Variant.h"
#include "mozilla/dom/FileSystemQuotaClientFactory.h"
#include "mozilla/dom/FlippedOnce.h"
#include "mozilla/dom/IndexedDatabaseManager.h"
#include "mozilla/dom/LocalStorageCommon.h"
#include "mozilla/dom/StorageDBUpdater.h"
#include "mozilla/dom/cache/QuotaClient.h"
#include "mozilla/dom/indexedDB/ActorsParent.h"
#include "mozilla/dom/ipc/IdType.h"
#include "mozilla/dom/localstorage/ActorsParent.h"
#include "mozilla/dom/quota/ArtificialFailure.h"
#include "mozilla/dom/quota/AssertionsImpl.h"
#include "mozilla/dom/quota/CheckedUnsafePtr.h"
#include "mozilla/dom/quota/Client.h"
#include "mozilla/dom/quota/ClientDirectoryLock.h"
#include "mozilla/dom/quota/ClientDirectoryLockHandle.h"
#include "mozilla/dom/quota/Config.h"
#include "mozilla/dom/quota/Constants.h"
#include "mozilla/dom/quota/DirectoryLockInlines.h"
#include "mozilla/dom/quota/FileUtils.h"
#include "mozilla/dom/quota/MozPromiseUtils.h"
#include "mozilla/dom/quota/OriginDirectoryLock.h"
#include "mozilla/dom/quota/PersistenceType.h"
#include "mozilla/dom/quota/PrincipalUtils.h"
#include "mozilla/dom/quota/QuotaManagerImpl.h"
#include "mozilla/dom/quota/QuotaManagerService.h"
#include "mozilla/dom/quota/ResultExtensions.h"
#include "mozilla/dom/quota/ScopedLogExtraInfo.h"
#include "mozilla/dom/quota/StreamUtils.h"
#include "mozilla/dom/quota/UniversalDirectoryLock.h"
#include "mozilla/dom/quota/ThreadUtils.h"
#include "mozilla/dom/simpledb/ActorsParent.h"
#include "mozilla/fallible.h"
#include "mozilla/glean/DomQuotaMetrics.h"
#include "mozilla/ipc/BackgroundChild.h"
#include "mozilla/ipc/BackgroundParent.h"
#include "mozilla/ipc/PBackgroundChild.h"
#include "mozilla/ipc/ProtocolUtils.h"
#include "mozilla/net/ExtensionProtocolHandler.h"
#include "mozilla/StorageOriginAttributes.h"
#include "nsAppDirectoryServiceDefs.h"
#include "nsBaseHashtable.h"
#include "nsCOMPtr.h"
#include "nsCRTGlue.h"
#include "nsClassHashtable.h"
#include "nsComponentManagerUtils.h"
#include "nsContentUtils.h"
#include "nsDebug.h"
#include "nsDirectoryServiceUtils.h"
#include "nsError.h"
#include "nsIBinaryInputStream.h"
#include "nsIBinaryOutputStream.h"
#include "nsIConsoleService.h"
#include "nsIDirectoryEnumerator.h"
#include "nsIDUtils.h"
#include "nsIEventTarget.h"
#include "nsIFile.h"
#include "nsIFileStreams.h"
#include "nsIInputStream.h"
#include "nsIObjectInputStream.h"
#include "nsIObjectOutputStream.h"
#include "nsIObserver.h"
#include "nsIObserverService.h"
#include "nsIOutputStream.h"
#include "nsIQuotaManagerServiceInternal.h"
#include "nsIQuotaRequests.h"
#include "nsIQuotaUtilsService.h"
#include "nsIPlatformInfo.h"
#include "nsIPrincipal.h"
#include "nsIRunnable.h"
#include "nsISupports.h"
#include "nsIThread.h"
#include "nsITimer.h"
#include "nsIURI.h"
#include "nsIWidget.h"
#include "nsLiteralString.h"
#include "nsNetUtil.h"
#include "nsPrintfCString.h"
#include "nsStandardURL.h"
#include "nsServiceManagerUtils.h"
#include "nsString.h"
#include "nsStringFlags.h"
#include "nsStringFwd.h"
#include "nsTArray.h"
#include "nsTHashtable.h"
#include "nsTLiteralString.h"
#include "nsTPromiseFlatString.h"
#include "nsTStringRepr.h"
#include "nsThreadUtils.h"
#include "nsURLHelper.h"
#include "nsXPCOM.h"
#include "nsXPCOMCID.h"
#include "nsXULAppAPI.h"
#include "prinrval.h"
#include "prio.h"
#include "prtime.h"
// The amount of time, in milliseconds, that our IO thread will stay alive
// after the last event it processes.
#define DEFAULT_THREAD_TIMEOUT_MS 30000
/**
* If shutdown takes this long, kill actors of a quota client, to avoid reaching
* the crash timeout.
*/
#define SHUTDOWN_KILL_ACTORS_TIMEOUT_MS 5000
/**
* Automatically crash the browser if shutdown of a quota client takes this
* long. We've chosen a value that is long enough that it is unlikely for the
* problem to be falsely triggered by slow system I/O. We've also chosen a
* value long enough so that automated tests should time out and fail if
* shutdown of a quota client takes too long. Also, this value is long enough
* so that testers can notice the timeout; we want to know about the timeouts,
* not hide them. On the other hand this value is less than 60 seconds which is
* used by nsTerminator to crash a hung main process.
*/
#define SHUTDOWN_CRASH_BROWSER_TIMEOUT_MS 45000
static_assert(
SHUTDOWN_CRASH_BROWSER_TIMEOUT_MS > SHUTDOWN_KILL_ACTORS_TIMEOUT_MS,
"The kill actors timeout must be shorter than the crash browser one.");
// profile-before-change, when we need to shut down quota manager
#define PROFILE_BEFORE_CHANGE_QM_OBSERVER_ID "profile-before-change-qm"
#define KB *1024ULL
#define MB *1024ULL KB
#define GB *1024ULL MB
namespace mozilla::dom::quota {
using namespace mozilla::ipc;
// We want profiles to be platform-independent so we always need to replace
// the same characters on every platform. Windows has the most extensive set
// of illegal characters so we use its FILE_ILLEGAL_CHARACTERS and
// FILE_PATH_SEPARATOR.
const char QuotaManager::kReplaceChars[] = CONTROL_CHARACTERS "/:*?\"<>|\\";
const char16_t QuotaManager::kReplaceChars16[] =
u"" CONTROL_CHARACTERS "/:*?\"<>|\\";
namespace {
/*******************************************************************************
* Constants
******************************************************************************/
const uint32_t kSQLitePageSizeOverride = 512;
// Important version history:
// - Bug 1290481 bumped our schema from major.minor 2.0 to 3.0 in Firefox 57
// which caused Firefox 57 release concerns because the major schema upgrade
// means anyone downgrading to Firefox 56 will experience a non-operational
// QuotaManager and all of its clients.
// - Bug 1404344 got very concerned about that and so we decided to effectively
// rename 3.0 to 2.1, effective in Firefox 57. This works because post
// storage.sqlite v1.0, QuotaManager doesn't care about minor storage version
// increases. It also works because all the upgrade did was give the DOM
// Cache API QuotaClient an opportunity to create its newly added .padding
// files during initialization/upgrade, which isn't functionally necessary as
// that can be done on demand.
// Major storage version. Bump for backwards-incompatible changes.
// (The next major version should be 4 to distinguish from the Bug 1290481
// downgrade snafu.)
const uint32_t kMajorStorageVersion = 2;
// Minor storage version. Bump for backwards-compatible changes.
const uint32_t kMinorStorageVersion = 3;
// The storage version we store in the SQLite database is a (signed) 32-bit
// integer. The major version is left-shifted 16 bits so the max value is
// 0xFFFF. The minor version occupies the lower 16 bits and its max is 0xFFFF.
static_assert(kMajorStorageVersion <= 0xFFFF,
"Major version needs to fit in 16 bits.");
static_assert(kMinorStorageVersion <= 0xFFFF,
"Minor version needs to fit in 16 bits.");
const int32_t kStorageVersion =
int32_t((kMajorStorageVersion << 16) + kMinorStorageVersion);
// See comments above about why these are a thing.
const int32_t kHackyPreDowngradeStorageVersion = int32_t((3 << 16) + 0);
const int32_t kHackyPostDowngradeStorageVersion = int32_t((2 << 16) + 1);
const char kAboutHomeOriginPrefix[] = "moz-safe-about:home";
const char kIndexedDBOriginPrefix[] = "indexeddb://";
const char kResourceOriginPrefix[] = "resource://";
constexpr auto kStorageName = u"storage"_ns;
#define INDEXEDDB_DIRECTORY_NAME u"indexedDB"
#define ARCHIVES_DIRECTORY_NAME u"archives"
#define PERSISTENT_DIRECTORY_NAME u"persistent"
#define PERMANENT_DIRECTORY_NAME u"permanent"
#define TEMPORARY_DIRECTORY_NAME u"temporary"
#define DEFAULT_DIRECTORY_NAME u"default"
#define PRIVATE_DIRECTORY_NAME u"private"
#define TOBEREMOVED_DIRECTORY_NAME u"to-be-removed"
#define WEB_APPS_STORE_FILE_NAME u"webappsstore.sqlite"
#define LS_ARCHIVE_FILE_NAME u"ls-archive.sqlite"
#define LS_ARCHIVE_TMP_FILE_NAME u"ls-archive-tmp.sqlite"
const int32_t kLocalStorageArchiveVersion = 4;
const char kProfileDoChangeTopic[] = "profile-do-change";
const char kContextualIdentityServiceLoadFinishedTopic[] =
"contextual-identity-service-load-finished";
const char kPrivateBrowsingObserverTopic[] = "last-pb-context-exited";
const int32_t kCacheVersion = 2;
/******************************************************************************
* SQLite functions
******************************************************************************/
int32_t MakeStorageVersion(uint32_t aMajorStorageVersion,
uint32_t aMinorStorageVersion) {
return int32_t((aMajorStorageVersion << 16) + aMinorStorageVersion);
}
uint32_t GetMajorStorageVersion(int32_t aStorageVersion) {
return uint32_t(aStorageVersion >> 16);
}
nsresult CreateTables(mozIStorageConnection* aConnection) {
AssertIsOnIOThread();
MOZ_ASSERT(aConnection);
// Table `database`
QM_TRY(MOZ_TO_RESULT(
aConnection->ExecuteSimpleSQL("CREATE TABLE database"
"( cache_version INTEGER NOT NULL DEFAULT 0"
");"_ns)));
#ifdef DEBUG
{
QM_TRY_INSPECT(const int32_t& storageVersion,
MOZ_TO_RESULT_INVOKE_MEMBER(aConnection, GetSchemaVersion));
MOZ_ASSERT(storageVersion == 0);
}
#endif
QM_TRY(MOZ_TO_RESULT(aConnection->SetSchemaVersion(kStorageVersion)));
return NS_OK;
}
Result<int32_t, nsresult> LoadCacheVersion(mozIStorageConnection& aConnection) {
AssertIsOnIOThread();
QM_TRY_INSPECT(const auto& stmt,
CreateAndExecuteSingleStepStatement<
SingleStepResult::ReturnNullIfNoResult>(
aConnection, "SELECT cache_version FROM database"_ns));
QM_TRY(OkIf(stmt), Err(NS_ERROR_FILE_CORRUPTED));
QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt32, 0));
}
nsresult SaveCacheVersion(mozIStorageConnection& aConnection,
int32_t aVersion) {
AssertIsOnIOThread();
QM_TRY_INSPECT(
const auto& stmt,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, aConnection, CreateStatement,
"UPDATE database SET cache_version = :version;"_ns));
QM_TRY(MOZ_TO_RESULT(stmt->BindInt32ByName("version"_ns, aVersion)));
QM_TRY(MOZ_TO_RESULT(stmt->Execute()));
return NS_OK;
}
nsresult CreateCacheTables(mozIStorageConnection& aConnection) {
AssertIsOnIOThread();
// Table `cache`
QM_TRY(MOZ_TO_RESULT(
aConnection.ExecuteSimpleSQL("CREATE TABLE cache"
"( valid INTEGER NOT NULL DEFAULT 0"
", build_id TEXT NOT NULL DEFAULT ''"
");"_ns)));
// Table `repository`
QM_TRY(
MOZ_TO_RESULT(aConnection.ExecuteSimpleSQL("CREATE TABLE repository"
"( id INTEGER PRIMARY KEY"
", name TEXT NOT NULL"
");"_ns)));
// Table `origin`
QM_TRY(MOZ_TO_RESULT(
aConnection.ExecuteSimpleSQL("CREATE TABLE origin"
"( repository_id INTEGER NOT NULL"
", suffix TEXT"
", group_ TEXT NOT NULL"
", origin TEXT NOT NULL"
", client_usages TEXT NOT NULL"
", usage INTEGER NOT NULL"
", last_access_time INTEGER NOT NULL"
", accessed INTEGER NOT NULL"
", persisted INTEGER NOT NULL"
", PRIMARY KEY (repository_id, origin)"
", FOREIGN KEY (repository_id) "
"REFERENCES repository(id) "
");"_ns)));
#ifdef DEBUG
{
QM_TRY_INSPECT(const int32_t& cacheVersion, LoadCacheVersion(aConnection));
MOZ_ASSERT(cacheVersion == 0);
}
#endif
QM_TRY(MOZ_TO_RESULT(SaveCacheVersion(aConnection, kCacheVersion)));
return NS_OK;
}
OkOrErr InvalidateCache(mozIStorageConnection& aConnection) {
AssertIsOnIOThread();
static constexpr auto kDeleteCacheQuery = "DELETE FROM origin;"_ns;
static constexpr auto kSetInvalidFlagQuery = "UPDATE cache SET valid = 0"_ns;
QM_TRY(QM_OR_ELSE_WARN(
// Expression.
([&]() -> OkOrErr {
mozStorageTransaction transaction(&aConnection,
/*aCommitOnComplete */ false);
QM_TRY(QM_TO_RESULT(transaction.Start()));
QM_TRY(QM_TO_RESULT(aConnection.ExecuteSimpleSQL(kDeleteCacheQuery)));
QM_TRY(
QM_TO_RESULT(aConnection.ExecuteSimpleSQL(kSetInvalidFlagQuery)));
QM_TRY(QM_TO_RESULT(transaction.Commit()));
return Ok{};
}()),
// Fallback.
([&](const QMResult& rv) -> OkOrErr {
QM_TRY(
QM_TO_RESULT(aConnection.ExecuteSimpleSQL(kSetInvalidFlagQuery)));
return Ok{};
})));
return Ok{};
}
nsresult UpgradeCacheFrom1To2(mozIStorageConnection& aConnection) {
AssertIsOnIOThread();
QM_TRY(MOZ_TO_RESULT(aConnection.ExecuteSimpleSQL(
"ALTER TABLE origin ADD COLUMN suffix TEXT"_ns)));
QM_TRY(InvalidateCache(aConnection));
#ifdef DEBUG
{
QM_TRY_INSPECT(const int32_t& cacheVersion, LoadCacheVersion(aConnection));
MOZ_ASSERT(cacheVersion == 1);
}
#endif
QM_TRY(MOZ_TO_RESULT(SaveCacheVersion(aConnection, 2)));
return NS_OK;
}
Result<bool, nsresult> MaybeCreateOrUpgradeCache(
mozIStorageConnection& aConnection) {
bool cacheUsable = true;
QM_TRY_UNWRAP(int32_t cacheVersion, LoadCacheVersion(aConnection));
if (cacheVersion > kCacheVersion) {
cacheUsable = false;
} else if (cacheVersion != kCacheVersion) {
const bool newCache = !cacheVersion;
mozStorageTransaction transaction(
&aConnection, false, mozIStorageConnection::TRANSACTION_IMMEDIATE);
QM_TRY(MOZ_TO_RESULT(transaction.Start()));
if (newCache) {
QM_TRY(MOZ_TO_RESULT(CreateCacheTables(aConnection)));
#ifdef DEBUG
{
QM_TRY_INSPECT(const int32_t& cacheVersion,
LoadCacheVersion(aConnection));
MOZ_ASSERT(cacheVersion == kCacheVersion);
}
#endif
QM_TRY(MOZ_TO_RESULT(aConnection.ExecuteSimpleSQL(
nsLiteralCString("INSERT INTO cache (valid, build_id) "
"VALUES (0, '')"))));
nsCOMPtr<mozIStorageStatement> insertStmt;
for (const PersistenceType persistenceType : kAllPersistenceTypes) {
if (insertStmt) {
MOZ_ALWAYS_SUCCEEDS(insertStmt->Reset());
} else {
QM_TRY_UNWRAP(insertStmt, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>,
aConnection, CreateStatement,
"INSERT INTO repository (id, name) "
"VALUES (:id, :name)"_ns));
}
QM_TRY(MOZ_TO_RESULT(
insertStmt->BindInt32ByName("id"_ns, persistenceType)));
QM_TRY(MOZ_TO_RESULT(insertStmt->BindUTF8StringByName(
"name"_ns, PersistenceTypeToString(persistenceType))));
QM_TRY(MOZ_TO_RESULT(insertStmt->Execute()));
}
} else {
// This logic needs to change next time we change the cache!
static_assert(kCacheVersion == 2,
"Upgrade function needed due to cache version increase.");
while (cacheVersion != kCacheVersion) {
if (cacheVersion == 1) {
QM_TRY(MOZ_TO_RESULT(UpgradeCacheFrom1To2(aConnection)));
} else {
QM_FAIL(Err(NS_ERROR_FAILURE), []() {
QM_WARNING(
"Unable to initialize cache, no upgrade path is "
"available!");
});
}
QM_TRY_UNWRAP(cacheVersion, LoadCacheVersion(aConnection));
}
MOZ_ASSERT(cacheVersion == kCacheVersion);
}
QM_TRY(MOZ_TO_RESULT(transaction.Commit()));
}
return cacheUsable;
}
Result<nsCOMPtr<mozIStorageConnection>, nsresult> CreateWebAppsStoreConnection(
nsIFile& aWebAppsStoreFile, mozIStorageService& aStorageService) {
AssertIsOnIOThread();
// Check if the old database exists at all.
QM_TRY_INSPECT(const bool& exists,
MOZ_TO_RESULT_INVOKE_MEMBER(aWebAppsStoreFile, Exists));
if (!exists) {
// webappsstore.sqlite doesn't exist, return a null connection.
return nsCOMPtr<mozIStorageConnection>{};
}
QM_TRY_INSPECT(const bool& isDirectory,
MOZ_TO_RESULT_INVOKE_MEMBER(aWebAppsStoreFile, IsDirectory));
if (isDirectory) {
QM_WARNING("webappsstore.sqlite is not a file!");
return nsCOMPtr<mozIStorageConnection>{};
}
QM_TRY_INSPECT(const auto& connection,
QM_OR_ELSE_WARN_IF(
// Expression.
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageConnection>, aStorageService,
OpenUnsharedDatabase, &aWebAppsStoreFile,
mozIStorageService::CONNECTION_DEFAULT),
// Predicate.
IsDatabaseCorruptionError,
// Fallback. Don't throw an error, leave a corrupted
// webappsstore database as it is.
ErrToDefaultOk<nsCOMPtr<mozIStorageConnection>>));
if (connection) {
// Don't propagate an error, leave a non-updateable webappsstore database as
// it is.
QM_TRY(MOZ_TO_RESULT(StorageDBUpdater::Update(connection)),
nsCOMPtr<mozIStorageConnection>{});
}
return connection;
}
Result<nsCOMPtr<nsIFile>, QMResult> GetLocalStorageArchiveFile(
const nsAString& aDirectoryPath) {
AssertIsOnIOThread();
MOZ_ASSERT(!aDirectoryPath.IsEmpty());
QM_TRY_UNWRAP(auto lsArchiveFile,
QM_TO_RESULT_TRANSFORM(QM_NewLocalFile(aDirectoryPath)));
QM_TRY(QM_TO_RESULT(
lsArchiveFile->Append(nsLiteralString(LS_ARCHIVE_FILE_NAME))));
return lsArchiveFile;
}
Result<nsCOMPtr<nsIFile>, nsresult> GetLocalStorageArchiveTmpFile(
const nsAString& aDirectoryPath) {
AssertIsOnIOThread();
MOZ_ASSERT(!aDirectoryPath.IsEmpty());
QM_TRY_UNWRAP(auto lsArchiveTmpFile, QM_NewLocalFile(aDirectoryPath));
QM_TRY(MOZ_TO_RESULT(
lsArchiveTmpFile->Append(nsLiteralString(LS_ARCHIVE_TMP_FILE_NAME))));
return lsArchiveTmpFile;
}
Result<bool, nsresult> IsLocalStorageArchiveInitialized(
mozIStorageConnection& aConnection) {
AssertIsOnIOThread();
QM_TRY_RETURN(
MOZ_TO_RESULT_INVOKE_MEMBER(aConnection, TableExists, "database"_ns));
}
nsresult InitializeLocalStorageArchive(mozIStorageConnection* aConnection) {
AssertIsOnIOThread();
MOZ_ASSERT(aConnection);
#ifdef DEBUG
{
QM_TRY_INSPECT(const auto& initialized,
IsLocalStorageArchiveInitialized(*aConnection));
MOZ_ASSERT(!initialized);
}
#endif
QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL(
"CREATE TABLE database(version INTEGER NOT NULL DEFAULT 0);"_ns)));
QM_TRY_INSPECT(
const auto& stmt,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, aConnection, CreateStatement,
"INSERT INTO database (version) VALUES (:version)"_ns));
QM_TRY(MOZ_TO_RESULT(stmt->BindInt32ByName("version"_ns, 0)));
QM_TRY(MOZ_TO_RESULT(stmt->Execute()));
return NS_OK;
}
Result<int32_t, nsresult> LoadLocalStorageArchiveVersion(
mozIStorageConnection& aConnection) {
AssertIsOnIOThread();
QM_TRY_INSPECT(const auto& stmt,
CreateAndExecuteSingleStepStatement<
SingleStepResult::ReturnNullIfNoResult>(
aConnection, "SELECT version FROM database"_ns));
QM_TRY(OkIf(stmt), Err(NS_ERROR_FILE_CORRUPTED));
QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt32, 0));
}
nsresult SaveLocalStorageArchiveVersion(mozIStorageConnection* aConnection,
int32_t aVersion) {
AssertIsOnIOThread();
MOZ_ASSERT(aConnection);
nsCOMPtr<mozIStorageStatement> stmt;
nsresult rv = aConnection->CreateStatement(
"UPDATE database SET version = :version;"_ns, getter_AddRefs(stmt));
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
rv = stmt->BindInt32ByName("version"_ns, aVersion);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
rv = stmt->Execute();
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
template <typename FileFunc, typename DirectoryFunc>
Result<mozilla::Ok, nsresult> CollectEachFileEntry(
nsIFile& aDirectory, const FileFunc& aFileFunc,
const DirectoryFunc& aDirectoryFunc) {
AssertIsOnIOThread();
return CollectEachFile(
aDirectory,
[&aFileFunc, &aDirectoryFunc](
const nsCOMPtr<nsIFile>& file) -> Result<mozilla::Ok, nsresult> {
QM_TRY_INSPECT(const auto& dirEntryKind, GetDirEntryKind(*file));
switch (dirEntryKind) {
case nsIFileKind::ExistsAsDirectory:
return aDirectoryFunc(file);
case nsIFileKind::ExistsAsFile:
return aFileFunc(file);
case nsIFileKind::DoesNotExist:
// Ignore files that got removed externally while iterating.
break;
}
return Ok{};
});
}
/******************************************************************************
* Quota manager class declarations
******************************************************************************/
} // namespace
class QuotaManager::Observer final : public nsIObserver {
static Observer* sInstance;
bool mPendingProfileChange;
bool mShutdownComplete;
public:
static nsresult Initialize();
static nsIObserver* GetInstance();
static void ShutdownCompleted();
private:
Observer() : mPendingProfileChange(false), mShutdownComplete(false) {
MOZ_ASSERT(NS_IsMainThread());
}
~Observer() { MOZ_ASSERT(NS_IsMainThread()); }
nsresult Init();
nsresult Shutdown();
NS_DECL_ISUPPORTS
NS_DECL_NSIOBSERVER
};
namespace {
/*******************************************************************************
* Local class declarations
******************************************************************************/
} // namespace
namespace {
class CollectOriginsHelper final : public Runnable {
uint64_t mMinSizeToBeFreed;
Mutex& mMutex;
CondVar mCondVar;
// The members below are protected by mMutex.
nsTArray<RefPtr<OriginDirectoryLock>> mLocks;
uint64_t mSizeToBeFreed;
bool mWaiting;
public:
CollectOriginsHelper(mozilla::Mutex& aMutex, uint64_t aMinSizeToBeFreed);
// Blocks the current thread until origins are collected on the main thread.
// The returned value contains an aggregate size of those origins.
int64_t BlockAndReturnOriginsForEviction(
nsTArray<RefPtr<OriginDirectoryLock>>& aLocks);
private:
~CollectOriginsHelper() = default;
NS_IMETHOD
Run() override;
};
/*******************************************************************************
* Other class declarations
******************************************************************************/
class RecordTimeDeltaHelper final : public Runnable {
const mozilla::glean::impl::Labeled<
mozilla::glean::impl::TimingDistributionMetric, DynamicLabel>& mMetric;
// TimeStamps that are set on the IO thread.
LazyInitializedOnceNotNull<const TimeStamp> mStartTime;
LazyInitializedOnceNotNull<const TimeStamp> mEndTime;
// A TimeStamp that is set on the main thread.
LazyInitializedOnceNotNull<const TimeStamp> mInitializedTime;
public:
explicit RecordTimeDeltaHelper(const mozilla::glean::impl::Labeled<
mozilla::glean::impl::TimingDistributionMetric,
DynamicLabel>& aMetric)
: Runnable("dom::quota::RecordTimeDeltaHelper"), mMetric(aMetric) {}
TimeStamp Start();
TimeStamp End();
private:
~RecordTimeDeltaHelper() = default;
NS_DECL_NSIRUNNABLE
};
/*******************************************************************************
* Helper classes
******************************************************************************/
/*******************************************************************************
* Helper Functions
******************************************************************************/
// Return whether the group was actually updated.
Result<bool, nsresult> MaybeUpdateGroupForOrigin(
OriginMetadata& aOriginMetadata) {
MOZ_ASSERT(!NS_IsMainThread());
bool updated = false;
if (aOriginMetadata.mOrigin.EqualsLiteral(kChromeOrigin)) {
if (!aOriginMetadata.mGroup.EqualsLiteral(kChromeOrigin)) {
aOriginMetadata.mGroup.AssignLiteral(kChromeOrigin);
updated = true;
}
} else {
nsCOMPtr<nsIPrincipal> principal =
BasePrincipal::CreateContentPrincipal(aOriginMetadata.mOrigin);
QM_TRY(MOZ_TO_RESULT(principal));
QM_TRY_INSPECT(const auto& baseDomain,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoCString, principal,
GetBaseDomain));
const nsCString upToDateGroup = baseDomain + aOriginMetadata.mSuffix;
if (aOriginMetadata.mGroup != upToDateGroup) {
aOriginMetadata.mGroup = upToDateGroup;
updated = true;
}
}
return updated;
}
Result<bool, nsresult> MaybeUpdateLastAccessTimeForOrigin(
FullOriginMetadata& aFullOriginMetadata) {
MOZ_ASSERT(!NS_IsMainThread());
if (aFullOriginMetadata.mLastAccessTime == INT64_MIN) {
QuotaManager* quotaManager = QuotaManager::Get();
MOZ_ASSERT(quotaManager);
QM_TRY_INSPECT(const auto& metadataFile,
quotaManager->GetOriginDirectory(aFullOriginMetadata));
QM_TRY(MOZ_TO_RESULT(
metadataFile->Append(nsLiteralString(METADATA_V2_FILE_NAME))));
QM_TRY_UNWRAP(int64_t timestamp, MOZ_TO_RESULT_INVOKE_MEMBER(
metadataFile, GetLastModifiedTime));
// Need to convert from milliseconds to microseconds.
MOZ_ASSERT((INT64_MAX / PR_USEC_PER_MSEC) > timestamp);
timestamp *= int64_t(PR_USEC_PER_MSEC);
aFullOriginMetadata.mLastAccessTime = timestamp;
return true;
}
return false;
}
} // namespace
void ReportInternalError(const char* aFile, uint32_t aLine, const char* aStr) {
// Get leaf of file path
for (const char* p = aFile; *p; ++p) {
if (*p == '/' && *(p + 1)) {
aFile = p + 1;
}
}
nsContentUtils::LogSimpleConsoleError(
NS_ConvertUTF8toUTF16(
nsPrintfCString("Quota %s: %s:%" PRIu32, aStr, aFile, aLine)),
"quota"_ns,
false /* Quota Manager is not active in private browsing mode */,
true /* Quota Manager runs always in a chrome context */);
}
namespace {
bool gInvalidateQuotaCache = false;
StaticAutoPtr<nsString> gBasePath;
StaticAutoPtr<nsString> gStorageName;
StaticAutoPtr<nsCString> gBuildId;
#ifdef DEBUG
bool gQuotaManagerInitialized = false;
#endif
StaticRefPtr<QuotaManager> gInstance;
mozilla::Atomic<bool> gShutdown(false);
// A time stamp that can only be accessed on the main thread.
TimeStamp gLastOSWake;
// XXX Move to QuotaManager once NormalOriginOperationBase is declared in a
// separate and includable file.
using NormalOriginOpArray =
nsTArray<CheckedUnsafePtr<NormalOriginOperationBase>>;
StaticAutoPtr<NormalOriginOpArray> gNormalOriginOps;
class StorageOperationBase {
protected:
struct OriginProps {
enum Type { eChrome, eContent, eObsolete, eInvalid };
NotNull<nsCOMPtr<nsIFile>> mDirectory;
nsString mLeafName;
nsCString mSpec;
OriginAttributes mAttrs;
int64_t mTimestamp;
OriginMetadata mOriginMetadata;
nsCString mOriginalSuffix;
LazyInitializedOnceEarlyDestructible<const PersistenceType>
mPersistenceType;
Type mType;
bool mNeedsRestore;
bool mNeedsRestore2;
bool mIgnore;
public:
explicit OriginProps(MovingNotNull<nsCOMPtr<nsIFile>> aDirectory)
: mDirectory(std::move(aDirectory)),
mTimestamp(0),
mType(eContent),
mNeedsRestore(false),
mNeedsRestore2(false),
mIgnore(false) {}
template <typename PersistenceTypeFunc>
nsresult Init(PersistenceTypeFunc&& aPersistenceTypeFunc);
};
nsTArray<OriginProps> mOriginProps;
nsCOMPtr<nsIFile> mDirectory;
public:
explicit StorageOperationBase(nsIFile* aDirectory) : mDirectory(aDirectory) {
AssertIsOnIOThread();
}
NS_INLINE_DECL_REFCOUNTING(StorageOperationBase)
protected:
virtual ~StorageOperationBase() = default;
nsresult GetDirectoryMetadata(nsIFile* aDirectory, int64_t& aTimestamp,
nsACString& aGroup, nsACString& aOrigin,
Nullable<bool>& aIsApp);
// Upgrade helper to load the contents of ".metadata-v2" files from previous
// schema versions. Although QuotaManager has a similar GetDirectoryMetadata2
// method, it is only intended to read current version ".metadata-v2" files.
// And unlike the old ".metadata" files, the ".metadata-v2" format can evolve
// because our "storage.sqlite" lets us track the overall version of the
// storage directory.
nsresult GetDirectoryMetadata2(nsIFile* aDirectory, int64_t& aTimestamp,
nsACString& aSuffix, nsACString& aGroup,
nsACString& aOrigin, bool& aIsApp);
int64_t GetOriginLastModifiedTime(const OriginProps& aOriginProps);
nsresult RemoveObsoleteOrigin(const OriginProps& aOriginProps);
/**
* Rename the origin if the origin string generation from nsIPrincipal
* changed. This consists of renaming the origin in the metadata files and
* renaming the origin directory itself. For simplicity, the origin in
* metadata files is not actually updated, but the metadata files are
* recreated instead.
*
* @param aOriginProps the properties of the origin to check.
*
* @return whether origin was renamed.
*/
Result<bool, nsresult> MaybeRenameOrigin(const OriginProps& aOriginProps);
nsresult ProcessOriginDirectories();
virtual nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) = 0;
};
class RepositoryOperationBase : public StorageOperationBase {
public:
explicit RepositoryOperationBase(nsIFile* aDirectory)
: StorageOperationBase(aDirectory) {}
nsresult ProcessRepository();
protected:
virtual ~RepositoryOperationBase() = default;
template <typename UpgradeMethod>
nsresult MaybeUpgradeClients(const OriginProps& aOriginsProps,
UpgradeMethod aMethod);
private:
virtual PersistenceType PersistenceTypeFromSpec(const nsCString& aSpec) = 0;
virtual nsresult PrepareOriginDirectory(OriginProps& aOriginProps,
bool* aRemoved) = 0;
virtual nsresult PrepareClientDirectory(nsIFile* aFile,
const nsAString& aLeafName,
bool& aRemoved);
};
class CreateOrUpgradeDirectoryMetadataHelper final
: public RepositoryOperationBase {
nsCOMPtr<nsIFile> mPermanentStorageDir;
// The legacy PersistenceType, before the default repository introduction.
enum class LegacyPersistenceType {
Persistent = 0,
Temporary
// The PersistenceType had also PERSISTENCE_TYPE_INVALID, but we don't need
// it here.
};
LazyInitializedOnce<const LegacyPersistenceType> mLegacyPersistenceType;
public:
explicit CreateOrUpgradeDirectoryMetadataHelper(nsIFile* aDirectory)
: RepositoryOperationBase(aDirectory) {}
nsresult Init();
private:
Maybe<LegacyPersistenceType> LegacyPersistenceTypeFromFile(nsIFile& aFile,
const fallible_t&);
PersistenceType PersistenceTypeFromLegacyPersistentSpec(
const nsCString& aSpec);
PersistenceType PersistenceTypeFromSpec(const nsCString& aSpec) override;
nsresult MaybeUpgradeOriginDirectory(nsIFile* aDirectory);
nsresult PrepareOriginDirectory(OriginProps& aOriginProps,
bool* aRemoved) override;
nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) override;
};
class UpgradeStorageHelperBase : public RepositoryOperationBase {
LazyInitializedOnce<const PersistenceType> mPersistenceType;
public:
explicit UpgradeStorageHelperBase(nsIFile* aDirectory)
: RepositoryOperationBase(aDirectory) {}
nsresult Init();
private:
PersistenceType PersistenceTypeFromSpec(const nsCString& aSpec) override;
};
class UpgradeStorageFrom0_0To1_0Helper final : public UpgradeStorageHelperBase {
public:
explicit UpgradeStorageFrom0_0To1_0Helper(nsIFile* aDirectory)
: UpgradeStorageHelperBase(aDirectory) {}
private:
nsresult PrepareOriginDirectory(OriginProps& aOriginProps,
bool* aRemoved) override;
nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) override;
};
class UpgradeStorageFrom1_0To2_0Helper final : public UpgradeStorageHelperBase {
public:
explicit UpgradeStorageFrom1_0To2_0Helper(nsIFile* aDirectory)
: UpgradeStorageHelperBase(aDirectory) {}
private:
nsresult MaybeRemoveMorgueDirectory(const OriginProps& aOriginProps);
/**
* Remove the origin directory if appId is present in origin attributes.
*
* @param aOriginProps the properties of the origin to check.
*
* @return whether the origin directory was removed.
*/
Result<bool, nsresult> MaybeRemoveAppsData(const OriginProps& aOriginProps);
nsresult PrepareOriginDirectory(OriginProps& aOriginProps,
bool* aRemoved) override;
nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) override;
};
class UpgradeStorageFrom2_0To2_1Helper final : public UpgradeStorageHelperBase {
public:
explicit UpgradeStorageFrom2_0To2_1Helper(nsIFile* aDirectory)
: UpgradeStorageHelperBase(aDirectory) {}
private:
nsresult PrepareOriginDirectory(OriginProps& aOriginProps,
bool* aRemoved) override;
nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) override;
};
class UpgradeStorageFrom2_1To2_2Helper final : public UpgradeStorageHelperBase {
public:
explicit UpgradeStorageFrom2_1To2_2Helper(nsIFile* aDirectory)
: UpgradeStorageHelperBase(aDirectory) {}
private:
nsresult PrepareOriginDirectory(OriginProps& aOriginProps,
bool* aRemoved) override;
nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) override;
nsresult PrepareClientDirectory(nsIFile* aFile, const nsAString& aLeafName,
bool& aRemoved) override;
};
class RestoreDirectoryMetadata2Helper final : public StorageOperationBase {
LazyInitializedOnce<const PersistenceType> mPersistenceType;
public:
explicit RestoreDirectoryMetadata2Helper(nsIFile* aDirectory)
: StorageOperationBase(aDirectory) {}
nsresult Init();
nsresult RestoreMetadata2File();
private:
nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) override;
};
Result<nsAutoString, nsresult> GetPathForStorage(
nsIFile& aBaseDir, const nsAString& aStorageName) {
QM_TRY_INSPECT(const auto& storageDir,
CloneFileAndAppend(aBaseDir, aStorageName));
QM_TRY_RETURN(
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoString, storageDir, GetPath));
}
int64_t GetLastModifiedTime(PersistenceType aPersistenceType, nsIFile& aFile) {
AssertIsOnIOThread();
class MOZ_STACK_CLASS Helper final {
public:
static nsresult GetLastModifiedTime(nsIFile* aFile, int64_t* aTimestamp) {
AssertIsOnIOThread();
MOZ_ASSERT(aFile);
MOZ_ASSERT(aTimestamp);
QM_TRY_INSPECT(const auto& dirEntryKind, GetDirEntryKind(*aFile));
switch (dirEntryKind) {
case nsIFileKind::ExistsAsDirectory:
QM_TRY(CollectEachFile(
*aFile,
[&aTimestamp](const nsCOMPtr<nsIFile>& file)
-> Result<mozilla::Ok, nsresult> {
QM_TRY(MOZ_TO_RESULT(GetLastModifiedTime(file, aTimestamp)));
return Ok{};
}));
break;
case nsIFileKind::ExistsAsFile: {
QM_TRY_INSPECT(const auto& leafName,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoString, aFile,
GetLeafName));
// Bug 1595445 will handle unknown files here.
if (IsOriginMetadata(leafName) || IsTempMetadata(leafName) ||
IsDotFile(leafName)) {
return NS_OK;
}
QM_TRY_UNWRAP(int64_t timestamp, MOZ_TO_RESULT_INVOKE_MEMBER(
aFile, GetLastModifiedTime));
// Need to convert from milliseconds to microseconds.
MOZ_ASSERT((INT64_MAX / PR_USEC_PER_MSEC) > timestamp);
timestamp *= int64_t(PR_USEC_PER_MSEC);
if (timestamp > *aTimestamp) {
*aTimestamp = timestamp;
}
break;
}
case nsIFileKind::DoesNotExist:
// Ignore files that got removed externally while iterating.
break;
}
return NS_OK;
}
};
if (aPersistenceType == PERSISTENCE_TYPE_PERSISTENT) {
return PR_Now();
}
int64_t timestamp = INT64_MIN;
nsresult rv = Helper::GetLastModifiedTime(&aFile, &timestamp);
if (NS_FAILED(rv)) {
timestamp = PR_Now();
}
// XXX if there were no suitable files for getting last modified time
// (timestamp is still set to INT64_MIN), we should return the current time
// instead of returning INT64_MIN.
return timestamp;
}
// Returns a bool indicating whether the directory was newly created.
Result<bool, nsresult> EnsureDirectory(nsIFile& aDirectory) {
AssertIsOnIOThread();
// Callers call this function without checking if the directory already
// exists (idempotent usage). QM_OR_ELSE_WARN_IF is not used here since we
// just want to log NS_ERROR_FILE_ALREADY_EXISTS result and not spam the
// reports.
QM_TRY_INSPECT(const auto& exists,
QM_OR_ELSE_LOG_VERBOSE_IF(
// Expression.
MOZ_TO_RESULT_INVOKE_MEMBER(aDirectory, Create,
nsIFile::DIRECTORY_TYPE, 0755,
/* aSkipAncestors = */ false)
.map([](Ok) { return false; }),
// Predicate.
IsSpecificError<NS_ERROR_FILE_ALREADY_EXISTS>,
// Fallback.
ErrToOk<true>));
if (exists) {
QM_TRY_INSPECT(const bool& isDirectory,
MOZ_TO_RESULT_INVOKE_MEMBER(aDirectory, IsDirectory));
QM_TRY(OkIf(isDirectory), Err(NS_ERROR_UNEXPECTED));
}
return !exists;
}
void GetJarPrefix(bool aInIsolatedMozBrowser, nsACString& aJarPrefix) {
aJarPrefix.Truncate();
// Fallback.
if (!aInIsolatedMozBrowser) {
return;
}
// AppId is an unused b2g identifier. Let's set it to 0 all the time (see bug
// 1320404).
// aJarPrefix = appId + "+" + { 't', 'f' } + "+";
aJarPrefix.AppendInt(0); // TODO: this is the appId, to be removed.
aJarPrefix.Append('+');
aJarPrefix.Append(aInIsolatedMozBrowser ? 't' : 'f');
aJarPrefix.Append('+');
}
// This method computes and returns our best guess for the temporary storage
// limit (in bytes), based on disk capacity.
Result<uint64_t, nsresult> GetTemporaryStorageLimit(nsIFile& aStorageDir) {
// The fixed limit pref can be used to override temporary storage limit
// calculation.
if (StaticPrefs::dom_quotaManager_temporaryStorage_fixedLimit() >= 0) {
return static_cast<uint64_t>(
StaticPrefs::dom_quotaManager_temporaryStorage_fixedLimit()) *
1024;
}
constexpr int64_t teraByte = (1024LL * 1024LL * 1024LL * 1024LL);
constexpr int64_t maxAllowedCapacity = 8LL * teraByte;
// Check for disk capacity of user's device on which storage directory lives.
int64_t diskCapacity = maxAllowedCapacity;
// Log error when default disk capacity is returned due to the error
QM_WARNONLY_TRY(MOZ_TO_RESULT(aStorageDir.GetDiskCapacity(&diskCapacity)));
MOZ_ASSERT(diskCapacity >= 0LL);
// Allow temporary storage to consume up to 50% of disk capacity.
int64_t capacityLimit = diskCapacity / 2LL;
// If the disk capacity reported by the operating system is very
// large and potentially incorrect due to hardware issues,
// a hardcoded limit is supplied instead.
QM_WARNONLY_TRY(
OkIf(capacityLimit < maxAllowedCapacity),
([&capacityLimit](const auto&) { capacityLimit = maxAllowedCapacity; }));
return capacityLimit;
}
bool IsOriginUnaccessed(const FullOriginMetadata& aFullOriginMetadata,
const int64_t aRecentTime) {
if (aFullOriginMetadata.mLastAccessTime > aRecentTime) {
return false;
}
return (aRecentTime - aFullOriginMetadata.mLastAccessTime) / PR_USEC_PER_SEC >
StaticPrefs::dom_quotaManager_unaccessedForLongTimeThresholdSec();
}
bool IsDirectoryLockBlockedBy(
const DirectoryLockImpl::PrepareInfo& aPrepareInfo,
const EnumSet<DirectoryLockCategory>& aCategories) {
const auto& locks = aPrepareInfo.BlockedOnRef();
return std::any_of(locks.cbegin(), locks.cend(),
[&aCategories](const auto& lock) {
return aCategories.contains(lock->Category());
});
}
bool IsDirectoryLockBlockedByUninitStorageOperation(
const DirectoryLockImpl::PrepareInfo& aPrepareInfo) {
return IsDirectoryLockBlockedBy(aPrepareInfo,
DirectoryLockCategory::UninitStorage);
}
bool IsDirectoryLockBlockedByUninitStorageOrUninitOriginsOperation(
const DirectoryLockImpl::PrepareInfo& aPrepareInfo) {
return IsDirectoryLockBlockedBy(aPrepareInfo,
{DirectoryLockCategory::UninitStorage,
DirectoryLockCategory::UninitOrigins});
}
} // namespace
/*******************************************************************************
* Exported functions
******************************************************************************/
void InitializeQuotaManager() {
MOZ_ASSERT(XRE_IsParentProcess());
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(!gQuotaManagerInitialized);
if (!QuotaManager::IsRunningGTests()) {
// These services have to be started on the main thread currently.
const nsCOMPtr<mozIStorageService> ss =
do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID);
QM_WARNONLY_TRY(OkIf(ss));
RefPtr<net::ExtensionProtocolHandler> extensionProtocolHandler =
net::ExtensionProtocolHandler::GetSingleton();
QM_WARNONLY_TRY(MOZ_TO_RESULT(extensionProtocolHandler));
IndexedDatabaseManager* mgr = IndexedDatabaseManager::GetOrCreate();
QM_WARNONLY_TRY(MOZ_TO_RESULT(mgr));
}
QM_WARNONLY_TRY(QM_TO_RESULT(QuotaManager::Initialize()));
#ifdef DEBUG
gQuotaManagerInitialized = true;
#endif
}
void InitializeScopedLogExtraInfo() {
#ifdef QM_SCOPED_LOG_EXTRA_INFO_ENABLED
ScopedLogExtraInfo::Initialize();
#endif
}
bool RecvShutdownQuotaManager() {
AssertIsOnBackgroundThread();
// If we are already in shutdown, don't call ShutdownInstance()
// again and return true immediately. We shall see this incident
// in Telemetry.
// XXX todo: Make QM_TRY stacks thread-aware (Bug 1735124)
// XXX todo: Active QM_TRY context for shutdown (Bug 1735170)
QM_TRY(OkIf(!gShutdown), true);
QuotaManager::ShutdownInstance();
return true;
}
QuotaManager::Observer* QuotaManager::Observer::sInstance = nullptr;
// static
nsresult QuotaManager::Observer::Initialize() {
MOZ_ASSERT(NS_IsMainThread());
RefPtr<Observer> observer = new Observer();
nsresult rv = observer->Init();
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
sInstance = observer;
return NS_OK;
}
// static
nsIObserver* QuotaManager::Observer::GetInstance() {
MOZ_ASSERT(NS_IsMainThread());
return sInstance;
}
// static
void QuotaManager::Observer::ShutdownCompleted() {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(sInstance);
sInstance->mShutdownComplete = true;
}
nsresult QuotaManager::Observer::Init() {
MOZ_ASSERT(NS_IsMainThread());
/**
* A RAII utility class to manage the registration and automatic
* unregistration of observers with `nsIObserverService`. This class is
* designed to simplify observer management, particularly when registering
* for multiple topics, by ensuring that already registered topics are
* unregistered if a failure occurs during subsequent registrations.
*/
class MOZ_RAII Registrar {
public:
Registrar(nsIObserverService* aObserverService, nsIObserver* aObserver,
const char* aTopic)
: mObserverService(std::move(aObserverService)),
mObserver(aObserver),
mTopic(aTopic),
mUnregisterOnDestruction(false) {
MOZ_ASSERT(aObserverService);
MOZ_ASSERT(aObserver);
MOZ_ASSERT(aTopic);
}
~Registrar() {
if (mUnregisterOnDestruction) {
mObserverService->RemoveObserver(mObserver, mTopic);
}
}
nsresult Register() {
MOZ_ASSERT(!mUnregisterOnDestruction);
nsresult rv = mObserverService->AddObserver(mObserver, mTopic, false);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
mUnregisterOnDestruction = true;
return NS_OK;
}
void Commit() { mUnregisterOnDestruction = false; }
private:
nsIObserverService* mObserverService;
nsIObserver* mObserver;
const char* mTopic;
bool mUnregisterOnDestruction;
};
nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
if (NS_WARN_IF(!obs)) {
return NS_ERROR_FAILURE;
}
Registrar xpcomShutdownRegistrar(obs, this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
QM_TRY(MOZ_TO_RESULT(xpcomShutdownRegistrar.Register()));
Registrar profileDoChangeRegistrar(obs, this, kProfileDoChangeTopic);
QM_TRY(MOZ_TO_RESULT(profileDoChangeRegistrar.Register()));
Registrar contextualIdentityServiceLoadFinishedRegistrar(
obs, this, kContextualIdentityServiceLoadFinishedTopic);
QM_TRY(
MOZ_TO_RESULT(contextualIdentityServiceLoadFinishedRegistrar.Register()));
Registrar profileBeforeChangeQmRegistrar(
obs, this, PROFILE_BEFORE_CHANGE_QM_OBSERVER_ID);
QM_TRY(MOZ_TO_RESULT(profileBeforeChangeQmRegistrar.Register()));
Registrar wakeNotificationRegistrar(obs, this, NS_WIDGET_WAKE_OBSERVER_TOPIC);
QM_TRY(MOZ_TO_RESULT(wakeNotificationRegistrar.Register()));
Registrar lastPbContextExitedRegistrar(obs, this,
kPrivateBrowsingObserverTopic);
QM_TRY(MOZ_TO_RESULT(lastPbContextExitedRegistrar.Register()));
xpcomShutdownRegistrar.Commit();
profileDoChangeRegistrar.Commit();
contextualIdentityServiceLoadFinishedRegistrar.Commit();
profileBeforeChangeQmRegistrar.Commit();
wakeNotificationRegistrar.Commit();
lastPbContextExitedRegistrar.Commit();
return NS_OK;
}
nsresult QuotaManager::Observer::Shutdown() {
MOZ_ASSERT(NS_IsMainThread());
nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
if (NS_WARN_IF(!obs)) {
return NS_ERROR_FAILURE;
}
MOZ_ALWAYS_SUCCEEDS(obs->RemoveObserver(this, kPrivateBrowsingObserverTopic));
MOZ_ALWAYS_SUCCEEDS(obs->RemoveObserver(this, NS_WIDGET_WAKE_OBSERVER_TOPIC));
MOZ_ALWAYS_SUCCEEDS(
obs->RemoveObserver(this, PROFILE_BEFORE_CHANGE_QM_OBSERVER_ID));
MOZ_ALWAYS_SUCCEEDS(
obs->RemoveObserver(this, kContextualIdentityServiceLoadFinishedTopic));
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;
}
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;
}
#ifdef XP_WIN
// Annotate if our profile lives on a network resource.
bool isNetworkPath = PathIsNetworkPathW(gBasePath->get());
CrashReporter::RecordAnnotationBool(
CrashReporter::Annotation::QuotaManagerStorageIsNetworkResource,
isNetworkPath);
#endif
QM_LOG(("Base path: %s", NS_ConvertUTF16toUTF8(*gBasePath).get()));
gStorageName = new nsString();
rv = Preferences::GetString("dom.quotaManager.storageName", *gStorageName);
if (NS_FAILED(rv)) {
*gStorageName = kStorageName;
}
gBuildId = new nsCString();
nsCOMPtr<nsIPlatformInfo> platformInfo =
do_GetService("@mozilla.org/xre/app-info;1");
if (NS_WARN_IF(!platformInfo)) {
return NS_ERROR_FAILURE;
}
rv = platformInfo->GetPlatformBuildID(*gBuildId);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
if (!strcmp(aTopic, kContextualIdentityServiceLoadFinishedTopic)) {
if (NS_WARN_IF(!gBasePath)) {
NS_WARNING(
"profile-do-change must precede "
"contextual-identity-service-load-finished!");
return NS_OK;
}
nsCOMPtr<nsIQuotaManagerServiceInternal> quotaManagerService =
QuotaManagerService::GetOrCreate();
if (NS_WARN_IF(!quotaManagerService)) {
return NS_ERROR_FAILURE;
}
nsCOMPtr<nsIQuotaUtilsService> quotaUtilsService =
do_GetService("@mozilla.org/dom/quota-utils-service;1");
if (NS_WARN_IF(!quotaUtilsService)) {
return NS_ERROR_FAILURE;
}
uint32_t thumbnailPrivateIdentityId;
nsresult rv = quotaUtilsService->GetPrivateIdentityId(
u"userContextIdInternal.thumbnail"_ns, &thumbnailPrivateIdentityId);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
rv = quotaManagerService->SetThumbnailPrivateIdentityId(
thumbnailPrivateIdentityId);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
if (!strcmp(aTopic, PROFILE_BEFORE_CHANGE_QM_OBSERVER_ID)) {
if (NS_WARN_IF(!gBasePath)) {
NS_WARNING("profile-do-change must precede profile-before-change-qm!");
return NS_OK;
}
// mPendingProfileChange is our re-entrancy guard (the nested event loop
// below may cause re-entrancy).
if (mPendingProfileChange) {
return NS_OK;
}
AutoRestore<bool> pending(mPendingProfileChange);
mPendingProfileChange = true;
mShutdownComplete = false;
PBackgroundChild* backgroundActor =
BackgroundChild::GetOrCreateForCurrentThread();
if (NS_WARN_IF(!backgroundActor)) {
return NS_ERROR_FAILURE;
}
if (NS_WARN_IF(!backgroundActor->SendShutdownQuotaManager())) {
return NS_ERROR_FAILURE;
}
MOZ_ALWAYS_TRUE(SpinEventLoopUntil(
"QuotaManager::Observer::Observe profile-before-change-qm"_ns,
[&]() { return mShutdownComplete; }));
gBasePath = nullptr;
gStorageName = nullptr;
gBuildId = nullptr;
return NS_OK;
}
if (!strcmp(aTopic, kPrivateBrowsingObserverTopic)) {
auto* const quotaManagerService = QuotaManagerService::GetOrCreate();
if (NS_WARN_IF(!quotaManagerService)) {
return NS_ERROR_FAILURE;
}
nsCOMPtr<nsIQuotaRequest> request;
rv = quotaManagerService->ClearStoragesForPrivateBrowsing(
nsGetterAddRefs(request));
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) {
rv = Shutdown();
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
if (!strcmp(aTopic, NS_WIDGET_WAKE_OBSERVER_TOPIC)) {
gLastOSWake = TimeStamp::Now();
return NS_OK;
}
NS_WARNING("Unknown observer topic!");
return NS_OK;
}
/*******************************************************************************
* Quota manager
******************************************************************************/
QuotaManager::QuotaManager(const nsAString& aBasePath,
const nsAString& aStorageName)
: mQuotaMutex("QuotaManager.mQuotaMutex"),
mBasePath(aBasePath),
mStorageName(aStorageName),
mTemporaryStorageUsage(0),
mNextDirectoryLockId(0),
mStorageInitialized(false),
mPersistentStorageInitialized(false),
mPersistentStorageInitializedInternal(false),
mTemporaryStorageInitialized(false),
mTemporaryStorageInitializedInternal(false),
mInitializingAllTemporaryOrigins(false),
mAllTemporaryOriginsInitialized(false),
mCacheUsable(false) {
AssertIsOnOwningThread();
MOZ_ASSERT(!gInstance);
}
QuotaManager::~QuotaManager() {
AssertIsOnOwningThread();
MOZ_ASSERT(!gInstance || gInstance == this);
}
// static
nsresult QuotaManager::Initialize() {
MOZ_ASSERT(NS_IsMainThread());
nsresult rv = Observer::Initialize();
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
// static
Result<MovingNotNull<RefPtr<QuotaManager>>, nsresult>
QuotaManager::GetOrCreate() {
AssertIsOnBackgroundThread();
if (gInstance) {
return WrapMovingNotNullUnchecked(RefPtr<QuotaManager>{gInstance});
}
QM_TRY(OkIf(gBasePath), Err(NS_ERROR_FAILURE), [](const auto&) {
NS_WARNING(
"Trying to create QuotaManager before profile-do-change! "
"Forgot to call do_get_profile()?");
});
QM_TRY(OkIf(!IsShuttingDown()), Err(NS_ERROR_FAILURE), [](const auto&) {
MOZ_ASSERT(false,
"Trying to create QuotaManager after profile-before-change-qm!");
});
auto instance = MakeRefPtr<QuotaManager>(*gBasePath, *gStorageName);
QM_TRY(MOZ_TO_RESULT(instance->Init()));
gInstance = instance;
// Do this before clients have a chance to acquire a directory lock for the
// private repository.
gInstance->ClearPrivateRepository();
return WrapMovingNotNullUnchecked(std::move(instance));
}
Result<Ok, nsresult> QuotaManager::EnsureCreated() {
AssertIsOnBackgroundThread();
QM_TRY_RETURN(GetOrCreate().map([](const auto& res) { return Ok{}; }))
}
// static
QuotaManager* QuotaManager::Get() {
// Does not return an owning reference.
return gInstance;
}
// static
nsIObserver* QuotaManager::GetObserver() {
MOZ_ASSERT(NS_IsMainThread());
return Observer::GetInstance();
}
// static
bool QuotaManager::IsShuttingDown() { return gShutdown; }
// static
void QuotaManager::ShutdownInstance() {
AssertIsOnBackgroundThread();
if (gInstance) {
auto recordTimeDeltaHelper =
MakeRefPtr<RecordTimeDeltaHelper>(glean::dom_quota::shutdown_time);
recordTimeDeltaHelper->Start();
// Glean SDK recommends using its own timing APIs where possible. In this
// case, we use NowExcludingSuspendMs() directly to manually calculate a
// duration that excludes suspend time. This is a valid exception because
// our use case is sensitive to suspend, and we need full control over the
// timing logic.
//
// We are currently recording both this and the older helper-based
// measurement. The results are not directly comparable, since the new API
// uses monotonic time. If this approach proves more reliable, we'll retire
// the old telemetry, change the expiration of the new metric to never,
// and add a matching "including suspend" version.
const auto startExcludingSuspendMs = NowExcludingSuspendMs();
gInstance->Shutdown();
const auto endExcludingSuspendMs = NowExcludingSuspendMs();
if (startExcludingSuspendMs && endExcludingSuspendMs) {
const auto duration = TimeDuration::FromMilliseconds(
*endExcludingSuspendMs - *startExcludingSuspendMs);
glean::quotamanager_shutdown::total_time_excluding_suspend
.AccumulateRawDuration(duration);
}
recordTimeDeltaHelper->End();
gInstance = nullptr;
} else {
// If we were never initialized, just set the flag to avoid late creation.
gShutdown = true;
}
RefPtr<Runnable> runnable =
NS_NewRunnableFunction("dom::quota::QuotaManager::ShutdownCompleted",
[]() { Observer::ShutdownCompleted(); });
MOZ_ASSERT(runnable);
MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(runnable.forget()));
}
// static
void QuotaManager::Reset() {
AssertIsOnBackgroundThread();
MOZ_ASSERT(!gInstance);
MOZ_ASSERT(gShutdown);
gShutdown = false;
}
// static
bool QuotaManager::IsOSMetadata(const nsAString& aFileName) {
return mozilla::dom::quota::IsOSMetadata(aFileName);
}
// static
bool QuotaManager::IsDotFile(const nsAString& aFileName) {
return mozilla::dom::quota::IsDotFile(aFileName);
}
void QuotaManager::RegisterNormalOriginOp(
NormalOriginOperationBase& aNormalOriginOp) {
AssertIsOnBackgroundThread();
if (!gNormalOriginOps) {
gNormalOriginOps = new NormalOriginOpArray();
}
gNormalOriginOps->AppendElement(&aNormalOriginOp);
}
void QuotaManager::UnregisterNormalOriginOp(
NormalOriginOperationBase& aNormalOriginOp) {
AssertIsOnBackgroundThread();
MOZ_ASSERT(gNormalOriginOps);
gNormalOriginOps->RemoveElement(&aNormalOriginOp);
if (gNormalOriginOps->IsEmpty()) {
gNormalOriginOps = nullptr;
}
}
void QuotaManager::RegisterDirectoryLock(DirectoryLockImpl& aLock) {
AssertIsOnOwningThread();
mDirectoryLocks.AppendElement(WrapNotNullUnchecked(&aLock));
if (aLock.ShouldUpdateLockIdTable()) {
MutexAutoLock lock(mQuotaMutex);
MOZ_DIAGNOSTIC_ASSERT(!mDirectoryLockIdTable.Contains(aLock.Id()));
mDirectoryLockIdTable.InsertOrUpdate(aLock.Id(),
WrapNotNullUnchecked(&aLock));
}
if (aLock.ShouldUpdateLockTable()) {
DirectoryLockTable& directoryLockTable =
GetDirectoryLockTable(aLock.GetPersistenceType());
// XXX It seems that the contents of the array are never actually used, we
// just use that like an inefficient use counter. Can't we just change
// DirectoryLockTable to a nsTHashMap<nsCStringHashKey, uint32_t>?
directoryLockTable
.LookupOrInsertWith(
aLock.Origin(),
[this, &aLock] {
if (!IsShuttingDown()) {
UpdateOriginAccessTime(aLock.GetPersistenceType(),
aLock.OriginMetadata());
}
return MakeUnique<nsTArray<NotNull<DirectoryLockImpl*>>>();
})
->AppendElement(WrapNotNullUnchecked(&aLock));
}
aLock.SetRegistered(true);
}
void QuotaManager::UnregisterDirectoryLock(DirectoryLockImpl& aLock) {
AssertIsOnOwningThread();
MOZ_ALWAYS_TRUE(mDirectoryLocks.RemoveElement(&aLock));
if (aLock.ShouldUpdateLockIdTable()) {
MutexAutoLock lock(mQuotaMutex);
MOZ_DIAGNOSTIC_ASSERT(mDirectoryLockIdTable.Contains(aLock.Id()));
mDirectoryLockIdTable.Remove(aLock.Id());
}
if (aLock.ShouldUpdateLockTable()) {
DirectoryLockTable& directoryLockTable =
GetDirectoryLockTable(aLock.GetPersistenceType());
// ClearDirectoryLockTables may have been called, so the element or entire
// array may not exist anymre.
nsTArray<NotNull<DirectoryLockImpl*>>* array;
if (directoryLockTable.Get(aLock.Origin(), &array) &&
array->RemoveElement(&aLock) && array->IsEmpty()) {
directoryLockTable.Remove(aLock.Origin());
if (!IsShuttingDown()) {
UpdateOriginAccessTime(aLock.GetPersistenceType(),
aLock.OriginMetadata());
}
}
}
aLock.SetRegistered(false);
}
void QuotaManager::AddPendingDirectoryLock(DirectoryLockImpl& aLock) {
AssertIsOnOwningThread();
mPendingDirectoryLocks.AppendElement(&aLock);
}
void QuotaManager::RemovePendingDirectoryLock(DirectoryLockImpl& aLock) {
AssertIsOnOwningThread();
MOZ_ALWAYS_TRUE(mPendingDirectoryLocks.RemoveElement(&aLock));
}
uint64_t QuotaManager::CollectOriginsForEviction(
uint64_t aMinSizeToBeFreed, nsTArray<RefPtr<OriginDirectoryLock>>& aLocks) {
AssertIsOnOwningThread();
MOZ_ASSERT(aLocks.IsEmpty());
// XXX This looks as if this could/should also use CollectLRUOriginInfosUntil,
// or maybe a generalization if that.
struct MOZ_STACK_CLASS Helper final {
static void GetInactiveOriginInfos(
const nsTArray<NotNull<RefPtr<OriginInfo>>>& aOriginInfos,
const nsTArray<NotNull<const DirectoryLockImpl*>>& aLocks,
OriginInfosFlatTraversable& aInactiveOriginInfos) {
for (const auto& originInfo : aOriginInfos) {
MOZ_ASSERT(originInfo->mGroupInfo->mPersistenceType !=
PERSISTENCE_TYPE_PERSISTENT);
if (originInfo->LockedPersisted()) {
continue;
}
// Never evict PERSISTENCE_TYPE_DEFAULT data associated to a
// moz-extension origin, unlike websites (which may more likely using
// the local data as a cache but still able to retrieve the same data
// from the server side) extensions do not have the same data stored
// anywhere else and evicting the data would result into potential data
// loss for the users.
//
// Also, unlike a website the extensions are explicitly installed and
// uninstalled by the user and all data associated to the extension
// principal will be completely removed once the addon is uninstalled.
if (originInfo->mGroupInfo->mPersistenceType !=
PERSISTENCE_TYPE_TEMPORARY &&
originInfo->IsExtensionOrigin()) {
continue;
}
const auto originScope =
OriginScope::FromOrigin(originInfo->FlattenToOriginMetadata());
const bool match =
std::any_of(aLocks.begin(), aLocks.end(),
[&originScope](const DirectoryLockImpl* const lock) {
return originScope.Matches(lock->GetOriginScope());
});
if (!match) {
MOZ_ASSERT(!originInfo->mCanonicalQuotaObjects.Count(),
"Inactive origin shouldn't have open files!");
aInactiveOriginInfos.InsertElementSorted(
originInfo, OriginInfoAccessTimeComparator());
}
}
}
};
// Split locks into separate arrays and filter out locks for persistent
// storage, they can't block us.
auto [temporaryStorageLocks, defaultStorageLocks,
privateStorageLocks] = [this] {
nsTArray<NotNull<const DirectoryLockImpl*>> temporaryStorageLocks;
nsTArray<NotNull<const DirectoryLockImpl*>> defaultStorageLocks;
nsTArray<NotNull<const DirectoryLockImpl*>> privateStorageLocks;
for (NotNull<const DirectoryLockImpl*> const lock : mDirectoryLocks) {
const PersistenceScope& persistenceScope = lock->PersistenceScopeRef();
if (persistenceScope.Matches(
PersistenceScope::CreateFromValue(PERSISTENCE_TYPE_TEMPORARY))) {
temporaryStorageLocks.AppendElement(lock);
}
if (persistenceScope.Matches(
PersistenceScope::CreateFromValue(PERSISTENCE_TYPE_DEFAULT))) {
defaultStorageLocks.AppendElement(lock);
}
if (persistenceScope.Matches(
PersistenceScope::CreateFromValue(PERSISTENCE_TYPE_PRIVATE))) {
privateStorageLocks.AppendElement(lock);
}
}
return std::make_tuple(std::move(temporaryStorageLocks),
std::move(defaultStorageLocks),
std::move(privateStorageLocks));
}();
// Enumerate and process inactive origins. This must be protected by the
// mutex.
MutexAutoLock lock(mQuotaMutex);
const auto [inactiveOrigins, sizeToBeFreed] =
[this, &temporaryStorageLocks = temporaryStorageLocks,
&defaultStorageLocks = defaultStorageLocks,
&privateStorageLocks = privateStorageLocks, aMinSizeToBeFreed] {
nsTArray<NotNull<RefPtr<const OriginInfo>>> inactiveOrigins;
for (const auto& entry : mGroupInfoPairs) {
const auto& pair = entry.GetData();
MOZ_ASSERT(!entry.GetKey().IsEmpty());
MOZ_ASSERT(pair);
RefPtr<GroupInfo> groupInfo =
pair->LockedGetGroupInfo(PERSISTENCE_TYPE_TEMPORARY);
if (groupInfo) {
Helper::GetInactiveOriginInfos(groupInfo->mOriginInfos,
temporaryStorageLocks,
inactiveOrigins);
}
groupInfo = pair->LockedGetGroupInfo(PERSISTENCE_TYPE_DEFAULT);
if (groupInfo) {
Helper::GetInactiveOriginInfos(
groupInfo->mOriginInfos, defaultStorageLocks, inactiveOrigins);
}
groupInfo = pair->LockedGetGroupInfo(PERSISTENCE_TYPE_PRIVATE);
if (groupInfo) {
Helper::GetInactiveOriginInfos(
groupInfo->mOriginInfos, privateStorageLocks, inactiveOrigins);
}
}
#ifdef DEBUG
// Make sure the array is sorted correctly.
const bool inactiveOriginsSorted =
std::is_sorted(inactiveOrigins.cbegin(), inactiveOrigins.cend(),
[](const auto& lhs, const auto& rhs) {
return lhs->mAccessTime < rhs->mAccessTime;
});
MOZ_ASSERT(inactiveOriginsSorted);
#endif
// Create a list of inactive and the least recently used origins
// whose aggregate size is greater or equals the minimal size to be
// freed.
uint64_t sizeToBeFreed = 0;
for (uint32_t count = inactiveOrigins.Length(), index = 0;
index < count; index++) {
if (sizeToBeFreed >= aMinSizeToBeFreed) {
inactiveOrigins.TruncateLength(index);
break;
}
sizeToBeFreed += inactiveOrigins[index]->LockedUsage();
}
return std::pair(std::move(inactiveOrigins), sizeToBeFreed);
}();
if (sizeToBeFreed >= aMinSizeToBeFreed) {
// Success, add directory locks for these origins, so any other
// operations for them will be delayed (until origin eviction is finalized).
for (const auto& originInfo : inactiveOrigins) {
auto lock = OriginDirectoryLock::CreateForEviction(
WrapNotNullUnchecked(this), originInfo->mGroupInfo->mPersistenceType,
originInfo->FlattenToOriginMetadata());
lock->AcquireImmediately();
aLocks.AppendElement(lock.forget());
}
return sizeToBeFreed;
}
return 0;
}
nsresult QuotaManager::Init() {
AssertIsOnOwningThread();
#ifdef XP_WIN
CacheUseDOSDevicePathSyntaxPrefValue();
#endif
QM_TRY_INSPECT(const auto& baseDir, QM_NewLocalFile(mBasePath));
QM_TRY_UNWRAP(
do_Init(mIndexedDBPath),
GetPathForStorage(*baseDir, nsLiteralString(INDEXEDDB_DIRECTORY_NAME)));
QM_TRY(MOZ_TO_RESULT(baseDir->Append(mStorageName)));
QM_TRY_UNWRAP(do_Init(mStoragePath),
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, baseDir, GetPath));
QM_TRY_UNWRAP(
do_Init(mStorageArchivesPath),
GetPathForStorage(*baseDir, nsLiteralString(ARCHIVES_DIRECTORY_NAME)));
QM_TRY_UNWRAP(
do_Init(mPermanentStoragePath),
GetPathForStorage(*baseDir, nsLiteralString(PERMANENT_DIRECTORY_NAME)));
QM_TRY_UNWRAP(
do_Init(mTemporaryStoragePath),
GetPathForStorage(*baseDir, nsLiteralString(TEMPORARY_DIRECTORY_NAME)));
QM_TRY_UNWRAP(
do_Init(mDefaultStoragePath),
GetPathForStorage(*baseDir, nsLiteralString(DEFAULT_DIRECTORY_NAME)));
QM_TRY_UNWRAP(
do_Init(mPrivateStoragePath),
GetPathForStorage(*baseDir, nsLiteralString(PRIVATE_DIRECTORY_NAME)));
QM_TRY_UNWRAP(
do_Init(mToBeRemovedStoragePath),
GetPathForStorage(*baseDir, nsLiteralString(TOBEREMOVED_DIRECTORY_NAME)));
QM_TRY_UNWRAP(do_Init(mIOThread),
MOZ_TO_RESULT_INVOKE_TYPED(
nsCOMPtr<nsIThread>, MOZ_SELECT_OVERLOAD(NS_NewNamedThread),
"QuotaManager IO"));
// XXX This could be eventually moved to nsThreadUtils.h or nsIThread
// could have an infallible method returning PRThread as return value.
auto PRThreadFromThread = [](nsIThread* aThread) {
MOZ_ASSERT(aThread);
PRThread* result;
MOZ_ALWAYS_SUCCEEDS(aThread->GetPRThread(&result));
MOZ_ASSERT(result);
return result;
};
mIOThreadAccessible.Transfer(PRThreadFromThread(*mIOThread));
static_assert(Client::IDB == 0 && Client::DOMCACHE == 1 && Client::SDB == 2 &&
Client::FILESYSTEM == 3 && Client::LS == 4 &&
Client::TYPE_MAX == 5,
"Fix the registration!");
// Register clients.
auto clients = decltype(mClients)::ValueType{};
clients.AppendElement(indexedDB::CreateQuotaClient());
clients.AppendElement(cache::CreateQuotaClient());
clients.AppendElement(simpledb::CreateQuotaClient());
clients.AppendElement(fs::CreateQuotaClient());
if (NextGenLocalStorageEnabled()) {
clients.AppendElement(localstorage::CreateQuotaClient());
} else {
clients.SetLength(Client::TypeMax());
}
mClients.init(std::move(clients));
MOZ_ASSERT(mClients->Capacity() == Client::TYPE_MAX,
"Should be using an auto array with correct capacity!");
mAllClientTypes.init(ClientTypesArray{
Client::Type::IDB, Client::Type::DOMCACHE, Client::Type::SDB,
Client::Type::FILESYSTEM, Client::Type::LS});
mAllClientTypesExceptLS.init(
ClientTypesArray{Client::Type::IDB, Client::Type::DOMCACHE,
Client::Type::SDB, Client::Type::FILESYSTEM});
return NS_OK;
}
// static
void QuotaManager::MaybeRecordQuotaClientShutdownStep(
const Client::Type aClientType, const nsACString& aStepDescription) {
// Callable on any thread.
auto* const quotaManager = QuotaManager::Get();
MOZ_DIAGNOSTIC_ASSERT(quotaManager);
if (quotaManager->IsShuttingDown()) {
quotaManager->RecordShutdownStep(Some(aClientType), aStepDescription);
}
}
// static
void QuotaManager::SafeMaybeRecordQuotaClientShutdownStep(
const Client::Type aClientType, const nsACString& aStepDescription) {
// Callable on any thread.
auto* const quotaManager = QuotaManager::Get();
if (quotaManager && quotaManager->IsShuttingDown()) {
quotaManager->RecordShutdownStep(Some(aClientType), aStepDescription);
}
}
void QuotaManager::RecordQuotaManagerShutdownStep(
const nsACString& aStepDescription) {
// Callable on any thread.
MOZ_ASSERT(IsShuttingDown());
RecordShutdownStep(Nothing{}, aStepDescription);
}
void QuotaManager::MaybeRecordQuotaManagerShutdownStep(
const nsACString& aStepDescription) {
// Callable on any thread.
if (IsShuttingDown()) {
RecordQuotaManagerShutdownStep(aStepDescription);
}
}
void QuotaManager::RecordShutdownStep(const Maybe<Client::Type> aClientType,
const nsACString& aStepDescription) {
MOZ_ASSERT(IsShuttingDown());
const TimeDuration elapsedSinceShutdownStart =
TimeStamp::NowLoRes() - *mShutdownStartedAt;
const auto stepString =
nsPrintfCString("%fs: %s", elapsedSinceShutdownStart.ToSeconds(),
nsPromiseFlatCString(aStepDescription).get());
if (aClientType) {
AssertIsOnBackgroundThread();
mShutdownSteps[*aClientType].Append(stepString + "\n"_ns);
} else {
// Callable on any thread.
MutexAutoLock lock(mQuotaMutex);
mQuotaManagerShutdownSteps.Append(stepString + "\n"_ns);
}
#ifdef DEBUG
// XXX Probably this isn't the mechanism that should be used here.
NS_DebugBreak(
NS_DEBUG_WARNING,
nsAutoCString(aClientType ? Client::TypeToText(*aClientType)
: "quota manager"_ns + " shutdown step"_ns)
.get(),
stepString.get(), __FILE__, __LINE__);
#endif
}
void QuotaManager::Shutdown() {
AssertIsOnOwningThread();
MOZ_DIAGNOSTIC_ASSERT(!gShutdown);
// Define some local helper functions
auto flagShutdownStarted = [this]() {
mShutdownStartedAt.init(TimeStamp::NowLoRes());
// Setting this flag prevents the service from being recreated and prevents
// further storages from being created.
gShutdown = true;
};
nsCOMPtr<nsITimer> crashBrowserTimer;
auto crashBrowserTimerCallback = [](nsITimer* aTimer, void* aClosure) {
auto* const quotaManager = static_cast<QuotaManager*>(aClosure);
quotaManager->RecordQuotaManagerShutdownStep(
"crashBrowserTimerCallback"_ns);
nsCString annotation;
for (Client::Type type : quotaManager->AllClientTypes()) {
auto& quotaClient = *(*quotaManager->mClients)[type];
if (!quotaClient.IsShutdownCompleted()) {
annotation.AppendPrintf("%s: %s\nIntermediate steps:\n%s\n\n",
Client::TypeToText(type).get(),
quotaClient.GetShutdownStatus().get(),
quotaManager->mShutdownSteps[type].get());
}
}
if (gNormalOriginOps) {
annotation.AppendPrintf("QM: %zu normal origin ops pending\n",
gNormalOriginOps->Length());
for (const auto& op : *gNormalOriginOps) {
#ifdef QM_COLLECTING_OPERATION_TELEMETRY
annotation.AppendPrintf("Op: %s pending\n", op->Name());
#endif
nsCString data;
op->Stringify(data);
annotation.AppendPrintf("Op details:\n%s\n", data.get());
}
}
{
MutexAutoLock lock(quotaManager->mQuotaMutex);
annotation.AppendPrintf("Intermediate steps:\n%s\n",
quotaManager->mQuotaManagerShutdownSteps.get());
}
CrashReporter::RecordAnnotationNSCString(
CrashReporter::Annotation::QuotaManagerShutdownTimeout, annotation);
MOZ_CRASH("Quota manager shutdown timed out");
};
auto startCrashBrowserTimer = [&]() {
crashBrowserTimer = NS_NewTimer();
MOZ_ASSERT(crashBrowserTimer);
if (crashBrowserTimer) {
RecordQuotaManagerShutdownStep("startCrashBrowserTimer"_ns);
MOZ_ALWAYS_SUCCEEDS(crashBrowserTimer->InitWithNamedFuncCallback(
crashBrowserTimerCallback, this, SHUTDOWN_CRASH_BROWSER_TIMEOUT_MS,
nsITimer::TYPE_ONE_SHOT,
"quota::QuotaManager::Shutdown::crashBrowserTimer"));
}
};
auto stopCrashBrowserTimer = [&]() {
if (crashBrowserTimer) {
RecordQuotaManagerShutdownStep("stopCrashBrowserTimer"_ns);
QM_WARNONLY_TRY(QM_TO_RESULT(crashBrowserTimer->Cancel()));
}
};
auto initiateShutdownWorkThreads = [this]() {
RecordQuotaManagerShutdownStep("initiateShutdownWorkThreads"_ns);
bool needsToWait = false;
for (Client::Type type : AllClientTypes()) {
// Clients are supposed to also AbortAllOperations from this point on
// to speed up shutdown, if possible. Thus pending operations
// might not be executed anymore.
needsToWait |= (*mClients)[type]->InitiateShutdownWorkThreads();
}
return needsToWait;
};
nsCOMPtr<nsITimer> killActorsTimer;
auto killActorsTimerCallback = [](nsITimer* aTimer, void* aClosure) {
auto* const quotaManager = static_cast<QuotaManager*>(aClosure);
quotaManager->RecordQuotaManagerShutdownStep("killActorsTimerCallback"_ns);
// XXX: This abort is a workaround to unblock shutdown, which
// ought to be removed by bug 1682326. We probably need more
// checks to immediately abort new operations during
// shutdown.
quotaManager->GetClient(Client::IDB)->AbortAllOperations();
for (Client::Type type : quotaManager->AllClientTypes()) {
quotaManager->GetClient(type)->ForceKillActors();
}
};
auto startKillActorsTimer = [&]() {
killActorsTimer = NS_NewTimer();
MOZ_ASSERT(killActorsTimer);
if (killActorsTimer) {
RecordQuotaManagerShutdownStep("startKillActorsTimer"_ns);
MOZ_ALWAYS_SUCCEEDS(killActorsTimer->InitWithNamedFuncCallback(
killActorsTimerCallback, this, SHUTDOWN_KILL_ACTORS_TIMEOUT_MS,
nsITimer::TYPE_ONE_SHOT,
"quota::QuotaManager::Shutdown::killActorsTimer"));
}
};
auto stopKillActorsTimer = [&]() {
if (killActorsTimer) {
RecordQuotaManagerShutdownStep("stopKillActorsTimer"_ns);
QM_WARNONLY_TRY(QM_TO_RESULT(killActorsTimer->Cancel()));
}
};
auto isAllClientsShutdownComplete = [this] {
return std::all_of(AllClientTypes().cbegin(), AllClientTypes().cend(),
[&self = *this](const auto type) {
return (*self.mClients)[type]->IsShutdownCompleted();
});
};
auto shutdownAndJoinWorkThreads = [this]() {
RecordQuotaManagerShutdownStep("shutdownAndJoinWorkThreads"_ns);
for (Client::Type type : AllClientTypes()) {
(*mClients)[type]->FinalizeShutdownWorkThreads();
}
};
auto shutdownAndJoinIOThread = [this]() {
RecordQuotaManagerShutdownStep("shutdownAndJoinIOThread"_ns);
// Make sure to join with our IO thread.
QM_WARNONLY_TRY(QM_TO_RESULT((*mIOThread)->Shutdown()));
};
auto invalidatePendingDirectoryLocks = [this]() {
RecordQuotaManagerShutdownStep("invalidatePendingDirectoryLocks"_ns);
for (RefPtr<DirectoryLockImpl>& lock : mPendingDirectoryLocks) {
lock->Invalidate();
}
};
// Body of the function
ScopedLogExtraInfo scope{ScopedLogExtraInfo::kTagContextTainted,
"dom::quota::QuotaManager::Shutdown"_ns};
// We always need to ensure that firefox does not shutdown with a private
// repository still on disk. They are ideally cleaned up on PBM session end
// but, in some cases like PBM autostart (i.e.
// browser.privatebrowsing.autostart), private repository could only be
// cleaned up on shutdown. ClearPrivateRepository below runs a async op and is
// better to do it before we run the ShutdownStorageOp since it expects all
// cleanup operations to be done by that point. We don't need to use the
// returned promise here because `ClearPrivateRepository` registers the
// underlying `ClearPrivateRepositoryOp` in `gNormalOriginOps`.
ClearPrivateRepository();
// This must be called before `flagShutdownStarted`, it would fail otherwise.
// `ShutdownStorageOp` needs to acquire an exclusive directory lock over
// entire <profile>/storage which will abort any existing operations and wait
// for all existing directory locks to be released. So the shutdown operation
// will effectively run after all existing operations.
// Similar, to ClearPrivateRepository operation above, ShutdownStorageOp also
// registers it's operation in `gNormalOriginOps` so we don't need to assign
// returned promise.
ShutdownStorage();
flagShutdownStarted();
startCrashBrowserTimer();
// XXX: StopIdleMaintenance now just notifies all clients to abort any
// maintenance work.
// This could be done as part of QuotaClient::AbortAllOperations.
StopIdleMaintenance();
// XXX In theory, we could simplify the code below (and also the `Client`
// interface) by removing the `initiateShutdownWorkThreads` and
// `isAllClientsShutdownComplete` calls because it should be sufficient
// to rely on `ShutdownStorage` to abort all existing operations and to
// wait for all existing directory locks to be released as well.
//
// This might not be possible after adding mInitializingAllTemporaryOrigins
// to the checks below.
const bool needsToWait = initiateShutdownWorkThreads() ||
static_cast<bool>(gNormalOriginOps) ||
mInitializingAllTemporaryOrigins;
// If any clients cannot shutdown immediately, spin the event loop while we
// wait on all the threads to close.
if (needsToWait) {
startKillActorsTimer();
MOZ_ALWAYS_TRUE(SpinEventLoopUntil(
"QuotaManager::Shutdown"_ns, [this, isAllClientsShutdownComplete]() {
return !gNormalOriginOps && isAllClientsShutdownComplete() &&
!mInitializingAllTemporaryOrigins;
}));
stopKillActorsTimer();
}
shutdownAndJoinWorkThreads();
shutdownAndJoinIOThread();
invalidatePendingDirectoryLocks();
stopCrashBrowserTimer();
}
void QuotaManager::InitQuotaForOrigin(
const FullOriginMetadata& aFullOriginMetadata,
const ClientUsageArray& aClientUsages, uint64_t aUsageBytes,
bool aDirectoryExists) {
AssertIsOnIOThread();
MOZ_ASSERT(IsBestEffortPersistenceType(aFullOriginMetadata.mPersistenceType));
MutexAutoLock lock(mQuotaMutex);
RefPtr<GroupInfo> groupInfo = LockedGetOrCreateGroupInfo(
aFullOriginMetadata.mPersistenceType, aFullOriginMetadata.mSuffix,
aFullOriginMetadata.mGroup);
groupInfo->LockedAddOriginInfo(MakeNotNull<RefPtr<OriginInfo>>(
groupInfo, aFullOriginMetadata.mOrigin,
aFullOriginMetadata.mStorageOrigin, aFullOriginMetadata.mIsPrivate,
aClientUsages, aUsageBytes, aFullOriginMetadata.mLastAccessTime,
aFullOriginMetadata.mPersisted, aDirectoryExists));
}
void QuotaManager::DecreaseUsageForClient(const ClientMetadata& aClientMetadata,
int64_t aSize) {
MOZ_ASSERT(!NS_IsMainThread());
MOZ_ASSERT(IsBestEffortPersistenceType(aClientMetadata.mPersistenceType));
MutexAutoLock lock(mQuotaMutex);
GroupInfoPair* pair;
if (!mGroupInfoPairs.Get(aClientMetadata.mGroup, &pair)) {
return;
}
RefPtr<GroupInfo> groupInfo =
pair->LockedGetGroupInfo(aClientMetadata.mPersistenceType);
if (!groupInfo) {
return;
}
RefPtr<OriginInfo> originInfo =
groupInfo->LockedGetOriginInfo(aClientMetadata.mOrigin);
if (originInfo) {
originInfo->LockedDecreaseUsage(aClientMetadata.mClientType, aSize);
}
}
void QuotaManager::ResetUsageForClient(const ClientMetadata& aClientMetadata) {
MOZ_ASSERT(!NS_IsMainThread());
MOZ_ASSERT(IsBestEffortPersistenceType(aClientMetadata.mPersistenceType));
MutexAutoLock lock(mQuotaMutex);
GroupInfoPair* pair;
if (!mGroupInfoPairs.Get(aClientMetadata.mGroup, &pair)) {
return;
}
RefPtr<GroupInfo> groupInfo =
pair->LockedGetGroupInfo(aClientMetadata.mPersistenceType);
if (!groupInfo) {
return;
}
RefPtr<OriginInfo> originInfo =
groupInfo->LockedGetOriginInfo(aClientMetadata.mOrigin);
if (originInfo) {
originInfo->LockedResetUsageForClient(aClientMetadata.mClientType);
}
}
UsageInfo QuotaManager::GetUsageForClient(PersistenceType aPersistenceType,
const OriginMetadata& aOriginMetadata,
Client::Type aClientType) {
MOZ_ASSERT(!NS_IsMainThread());
MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT);
MutexAutoLock lock(mQuotaMutex);
GroupInfoPair* pair;
if (!mGroupInfoPairs.Get(aOriginMetadata.mGroup, &pair)) {
return UsageInfo{};
}
RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(aPersistenceType);
if (!groupInfo) {
return UsageInfo{};
}
RefPtr<OriginInfo> originInfo =
groupInfo->LockedGetOriginInfo(aOriginMetadata.mOrigin);
if (!originInfo) {
return UsageInfo{};
}
return originInfo->LockedGetUsageForClient(aClientType);
}
void QuotaManager::UpdateOriginAccessTime(
PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata) {
AssertIsOnOwningThread();
MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT);
MOZ_ASSERT(aOriginMetadata.mPersistenceType == aPersistenceType);
MOZ_ASSERT(!IsShuttingDown());
if (!StaticPrefs::
dom_quotaManager_temporaryStorage_updateOriginAccessTime()) {
return;
}
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);
SaveOriginAccessTime(aOriginMetadata, timestamp);
}
}
void QuotaManager::RemoveQuota() {
AssertIsOnIOThread();
MutexAutoLock lock(mQuotaMutex);
for (const auto& entry : mGroupInfoPairs) {
const auto& pair = entry.GetData();
MOZ_ASSERT(!entry.GetKey().IsEmpty());
MOZ_ASSERT(pair);
RefPtr<GroupInfo> groupInfo =
pair->LockedGetGroupInfo(PERSISTENCE_TYPE_TEMPORARY);
if (groupInfo) {
groupInfo->LockedRemoveOriginInfos();
}
groupInfo = pair->LockedGetGroupInfo(PERSISTENCE_TYPE_DEFAULT);
if (groupInfo) {
groupInfo->LockedRemoveOriginInfos();
}
groupInfo = pair->LockedGetGroupInfo(PERSISTENCE_TYPE_PRIVATE);
if (groupInfo) {
groupInfo->LockedRemoveOriginInfos();
}
}
mGroupInfoPairs.Clear();
MOZ_ASSERT(mTemporaryStorageUsage == 0, "Should be zero!");
}
// XXX Rename this method because the method doesn't load full quota
// information if origin initialization is done lazily.
nsresult QuotaManager::LoadQuota() {
AssertIsOnIOThread();
MOZ_ASSERT(mStorageConnection);
MOZ_ASSERT(!mTemporaryStorageInitializedInternal);
// A list of all unaccessed default or temporary origins.
nsTArray<FullOriginMetadata> unaccessedOrigins;
// XXX The list of all unaccessed default or temporary origins can be now
// generated from mAllTemporaryOrigins.
auto MaybeCollectUnaccessedOrigin =
[loadQuotaInfoStartTime = PR_Now(),
&unaccessedOrigins](const auto& fullOriginMetadata) {
if (IsOriginUnaccessed(fullOriginMetadata, loadQuotaInfoStartTime)) {
unaccessedOrigins.AppendElement(fullOriginMetadata);
}
};
auto recordTimeDeltaHelper =
MakeRefPtr<RecordTimeDeltaHelper>(glean::dom_quota::info_load_time);
const auto startTime = recordTimeDeltaHelper->Start();
auto LoadQuotaFromCache = [&]() -> nsresult {
QM_TRY_INSPECT(
const auto& stmt,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, mStorageConnection, CreateStatement,
"SELECT repository_id, suffix, group_, "
"origin, client_usages, usage, "
"last_access_time, accessed, persisted "
"FROM origin"_ns));
auto autoRemoveQuota = MakeScopeExit([&] {
RemoveQuota();
RemoveTemporaryOrigins();
unaccessedOrigins.Clear();
});
QM_TRY(quota::CollectWhileHasResult(
*stmt,
[this,
&MaybeCollectUnaccessedOrigin](auto& stmt) -> Result<Ok, nsresult> {
QM_TRY_INSPECT(const int32_t& repositoryId,
MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt32, 0));
const auto maybePersistenceType =
PersistenceTypeFromInt32(repositoryId, fallible);
QM_TRY(OkIf(maybePersistenceType.isSome()), Err(NS_ERROR_FAILURE));
FullOriginMetadata fullOriginMetadata;
fullOriginMetadata.mPersistenceType = maybePersistenceType.value();
QM_TRY_UNWRAP(fullOriginMetadata.mSuffix,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, stmt,
GetUTF8String, 1));
QM_TRY_UNWRAP(fullOriginMetadata.mGroup,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, stmt,
GetUTF8String, 2));
QM_TRY_UNWRAP(fullOriginMetadata.mOrigin,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, stmt,
GetUTF8String, 3));
fullOriginMetadata.mStorageOrigin = fullOriginMetadata.mOrigin;
const auto extraInfo =
ScopedLogExtraInfo{ScopedLogExtraInfo::kTagStorageOriginTainted,
fullOriginMetadata.mStorageOrigin};
fullOriginMetadata.mIsPrivate = false;
QM_TRY_INSPECT(const auto& clientUsagesText,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, stmt,
GetUTF8String, 4));
ClientUsageArray clientUsages;
QM_TRY(MOZ_TO_RESULT(clientUsages.Deserialize(clientUsagesText)));
QM_TRY_INSPECT(const int64_t& usage,
MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt64, 5));
QM_TRY_UNWRAP(fullOriginMetadata.mLastAccessTime,
MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt64, 6));
QM_TRY_INSPECT(const int64_t& accessed,
MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt32, 7));
QM_TRY_UNWRAP(fullOriginMetadata.mPersisted,
MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt32, 8));
QM_TRY_INSPECT(const bool& groupUpdated,
MaybeUpdateGroupForOrigin(fullOriginMetadata));
Unused << groupUpdated;
QM_TRY_INSPECT(
const bool& lastAccessTimeUpdated,
MaybeUpdateLastAccessTimeForOrigin(fullOriginMetadata));
Unused << lastAccessTimeUpdated;
// We don't need to update the .metadata-v2 file on disk here,
// EnsureTemporaryOriginIsInitializedInternal is responsible for
// doing that. We just need to use correct group and last access time
// before initializing quota for the given origin. (Note that calling
// LoadFullOriginMetadataWithRestore below might update the group in
// the metadata file, but only as a side-effect. The actual place we
// ensure consistency is in
// EnsureTemporaryOriginIsInitializedInternal.)
if (accessed) {
QM_TRY_INSPECT(const auto& directory,
GetOriginDirectory(fullOriginMetadata));
QM_TRY_INSPECT(const bool& exists,
MOZ_TO_RESULT_INVOKE_MEMBER(directory, Exists));
QM_TRY(OkIf(exists), Err(NS_ERROR_FILE_NOT_FOUND));
QM_TRY_INSPECT(const bool& isDirectory,
MOZ_TO_RESULT_INVOKE_MEMBER(directory, IsDirectory));
QM_TRY(OkIf(isDirectory), Err(NS_ERROR_FILE_DESTINATION_NOT_DIR));
// Calling LoadFullOriginMetadataWithRestore might update the group
// in the metadata file, but only as a side-effect. The actual place
// we ensure consistency is in
// EnsureTemporaryOriginIsInitializedInternal.
QM_TRY_INSPECT(const auto& metadata,
LoadFullOriginMetadataWithRestore(directory));
QM_WARNONLY_TRY(OkIf(fullOriginMetadata.mLastAccessTime ==
metadata.mLastAccessTime));
QM_TRY(OkIf(fullOriginMetadata.mPersisted == metadata.mPersisted),
Err(NS_ERROR_FAILURE));
QM_TRY(OkIf(fullOriginMetadata.mPersistenceType ==
metadata.mPersistenceType),
Err(NS_ERROR_FAILURE));
QM_TRY(OkIf(fullOriginMetadata.mSuffix == metadata.mSuffix),
Err(NS_ERROR_FAILURE));
QM_TRY(OkIf(fullOriginMetadata.mGroup == metadata.mGroup),
Err(NS_ERROR_FAILURE));
QM_TRY(OkIf(fullOriginMetadata.mOrigin == metadata.mOrigin),
Err(NS_ERROR_FAILURE));
QM_TRY(OkIf(fullOriginMetadata.mStorageOrigin ==
metadata.mStorageOrigin),
Err(NS_ERROR_FAILURE));
QM_TRY(OkIf(fullOriginMetadata.mIsPrivate == metadata.mIsPrivate),
Err(NS_ERROR_FAILURE));
MaybeCollectUnaccessedOrigin(fullOriginMetadata);
AddTemporaryOrigin(fullOriginMetadata);
QM_TRY(MOZ_TO_RESULT(InitializeOrigin(
fullOriginMetadata.mPersistenceType, fullOriginMetadata,
fullOriginMetadata.mLastAccessTime,
fullOriginMetadata.mPersisted, directory)));
} else {
MaybeCollectUnaccessedOrigin(fullOriginMetadata);
AddTemporaryOrigin(fullOriginMetadata);
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_MEMBER(stmt, GetInt32, 0));
if (valid) {
if (!StaticPrefs::dom_quotaManager_caching_checkBuildId()) {
return true;
}
QM_TRY_INSPECT(const auto& buildId,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsAutoCString, stmt, GetUTF8String, 1));
return buildId == *gBuildId;
}
}
return false;
}()));
auto autoRemoveQuota = MakeScopeExit([&] {
RemoveQuota();
RemoveTemporaryOrigins();
});
if (!loadQuotaFromCache ||
!StaticPrefs::dom_quotaManager_loadQuotaFromCache() ||
![&LoadQuotaFromCache] {
QM_WARNONLY_TRY_UNWRAP(auto res, MOZ_TO_RESULT(LoadQuotaFromCache()));
return static_cast<bool>(res);
}()) {
// A keeper to defer the return only in Nightly, so that the telemetry data
// for whole profile can be collected.
#ifdef NIGHTLY_BUILD
nsresult statusKeeper = NS_OK;
#endif
const auto statusKeeperFunc = [&](const nsresult rv) {
RECORD_IN_NIGHTLY(statusKeeper, rv);
};
for (const PersistenceType type :
kInitializableBestEffortPersistenceTypes) {
if (NS_WARN_IF(IsShuttingDown())) {
RETURN_STATUS_OR_RESULT(statusKeeper, NS_ERROR_ABORT);
}
QM_TRY(([&]() -> Result<Ok, nsresult> {
QM_TRY(MOZ_TO_RESULT(([this, type, &MaybeCollectUnaccessedOrigin] {
const auto innerFunc = [&](const auto&) -> nsresult {
return InitializeRepository(type,
MaybeCollectUnaccessedOrigin);
};
return ExecuteInitialization(
type == PERSISTENCE_TYPE_DEFAULT
? Initialization::DefaultRepository
: Initialization::TemporaryRepository,
innerFunc);
}())),
OK_IN_NIGHTLY_PROPAGATE_IN_OTHERS, statusKeeperFunc);
return Ok{};
}()));
}
#ifdef NIGHTLY_BUILD
if (NS_FAILED(statusKeeper)) {
return statusKeeper;
}
#endif
}
autoRemoveQuota.release();
const auto endTime = recordTimeDeltaHelper->End();
if (StaticPrefs::dom_quotaManager_checkQuotaInfoLoadTime() &&
static_cast<uint32_t>((endTime - startTime).ToMilliseconds()) >=
StaticPrefs::dom_quotaManager_longQuotaInfoLoadTimeThresholdMs() &&
!unaccessedOrigins.IsEmpty()) {
QM_WARNONLY_TRY(ArchiveOrigins(unaccessedOrigins));
}
return NS_OK;
}
void QuotaManager::UnloadQuota() {
AssertIsOnIOThread();
MOZ_ASSERT(mStorageConnection);
MOZ_ASSERT(mTemporaryStorageInitializedInternal);
MOZ_ASSERT(mCacheUsable);
auto autoRemoveQuota = MakeScopeExit([&] { RemoveQuota(); });
mozStorageTransaction transaction(
mStorageConnection, false, mozIStorageConnection::TRANSACTION_IMMEDIATE);
QM_TRY(MOZ_TO_RESULT(transaction.Start()), QM_VOID);
QM_TRY(MOZ_TO_RESULT(
mStorageConnection->ExecuteSimpleSQL("DELETE FROM origin;"_ns)),
QM_VOID);
nsCOMPtr<mozIStorageStatement> insertStmt;
{
MutexAutoLock lock(mQuotaMutex);
for (auto iter = mGroupInfoPairs.Iter(); !iter.Done(); iter.Next()) {
MOZ_ASSERT(!iter.Key().IsEmpty());
GroupInfoPair* const pair = iter.UserData();
MOZ_ASSERT(pair);
for (const PersistenceType type : kBestEffortPersistenceTypes) {
RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(type);
if (!groupInfo) {
continue;
}
for (const auto& originInfo : groupInfo->mOriginInfos) {
MOZ_ASSERT(!originInfo->mCanonicalQuotaObjects.Count());
if (!originInfo->LockedDirectoryExists()) {
continue;
}
if (originInfo->mIsPrivate) {
continue;
}
if (insertStmt) {
MOZ_ALWAYS_SUCCEEDS(insertStmt->Reset());
} else {
QM_TRY_UNWRAP(
insertStmt,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, mStorageConnection,
CreateStatement,
"INSERT INTO origin (repository_id, suffix, group_, "
"origin, client_usages, usage, last_access_time, "
"accessed, persisted) "
"VALUES (:repository_id, :suffix, :group_, :origin, "
":client_usages, :usage, :last_access_time, :accessed, "
":persisted)"_ns),
QM_VOID);
}
QM_TRY(MOZ_TO_RESULT(originInfo->LockedBindToStatement(insertStmt)),
QM_VOID);
QM_TRY(MOZ_TO_RESULT(insertStmt->Execute()), QM_VOID);
}
groupInfo->LockedRemoveOriginInfos();
}
iter.Remove();
}
}
QM_TRY_INSPECT(
const auto& stmt,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, mStorageConnection, CreateStatement,
"UPDATE cache SET valid = :valid, build_id = :buildId;"_ns),
QM_VOID);
QM_TRY(MOZ_TO_RESULT(stmt->BindInt32ByName("valid"_ns, 1)), QM_VOID);
QM_TRY(MOZ_TO_RESULT(stmt->BindUTF8StringByName("buildId"_ns, *gBuildId)),
QM_VOID);
QM_TRY(MOZ_TO_RESULT(stmt->Execute()), QM_VOID);
QM_TRY(MOZ_TO_RESULT(transaction.Commit()), QM_VOID);
}
void QuotaManager::RemoveOriginFromCache(
const OriginMetadata& aOriginMetadata) {
AssertIsOnIOThread();
MOZ_ASSERT(mStorageConnection);
MOZ_ASSERT(!mTemporaryStorageInitializedInternal);
if (!mCacheUsable) {
return;
}
mozStorageTransaction transaction(
mStorageConnection, false, mozIStorageConnection::TRANSACTION_IMMEDIATE);
QM_TRY_INSPECT(
const auto& stmt,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, mStorageConnection, CreateStatement,
"DELETE FROM origin WHERE repository_id = :repository_id AND suffix = :suffix AND group_ = :group AND origin = :origin;"_ns),
QM_VOID);
QM_TRY(MOZ_TO_RESULT(stmt->BindInt32ByName("repository_id"_ns,
aOriginMetadata.mPersistenceType)),
QM_VOID);
QM_TRY(MOZ_TO_RESULT(
stmt->BindUTF8StringByName("suffix"_ns, aOriginMetadata.mSuffix)),
QM_VOID);
QM_TRY(MOZ_TO_RESULT(
stmt->BindUTF8StringByName("group"_ns, aOriginMetadata.mGroup)),
QM_VOID);
QM_TRY(MOZ_TO_RESULT(
stmt->BindUTF8StringByName("origin"_ns, aOriginMetadata.mOrigin)),
QM_VOID);
QM_TRY(MOZ_TO_RESULT(stmt->Execute()), QM_VOID);
QM_TRY(MOZ_TO_RESULT(transaction.Commit()), QM_VOID);
}
already_AddRefed<QuotaObject> QuotaManager::GetQuotaObject(
PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata,
Client::Type aClientType, nsIFile* aFile, int64_t aFileSize,
int64_t* aFileSizeOut /* = nullptr */) {
NS_ASSERTION(!NS_IsMainThread(), "Wrong thread!");
MOZ_ASSERT(aOriginMetadata.mPersistenceType == aPersistenceType);
if (aFileSizeOut) {
*aFileSizeOut = 0;
}
if (aPersistenceType == PERSISTENCE_TYPE_PERSISTENT) {
return nullptr;
}
QM_TRY_INSPECT(const auto& path,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, aFile, GetPath),
nullptr);
#ifdef DEBUG
{
QM_TRY_INSPECT(const auto& directory, GetOriginDirectory(aOriginMetadata),
nullptr);
nsAutoString clientType;
QM_TRY(OkIf(Client::TypeToText(aClientType, clientType, fallible)),
nullptr);
QM_TRY(MOZ_TO_RESULT(directory->Append(clientType)), nullptr);
QM_TRY_INSPECT(
const auto& directoryPath,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, directory, GetPath),
nullptr);
MOZ_ASSERT(StringBeginsWith(path, directoryPath));
}
#endif
QM_TRY_INSPECT(
const int64_t fileSize,
([&aFile, aFileSize]() -> Result<int64_t, nsresult> {
if (aFileSize == -1) {
QM_TRY_INSPECT(const bool& exists,
MOZ_TO_RESULT_INVOKE_MEMBER(aFile, Exists));
if (exists) {
QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(aFile, GetFileSize));
}
return 0;
}
return aFileSize;
}()),
nullptr);
RefPtr<QuotaObject> result;
{
MutexAutoLock lock(mQuotaMutex);
GroupInfoPair* pair;
if (!mGroupInfoPairs.Get(aOriginMetadata.mGroup, &pair)) {
return nullptr;
}
RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(aPersistenceType);
if (!groupInfo) {
return nullptr;
}
RefPtr<OriginInfo> originInfo =
groupInfo->LockedGetOriginInfo(aOriginMetadata.mOrigin);
if (!originInfo) {
return nullptr;
}
// We need this extra raw pointer because we can't assign to the smart
// pointer directly since QuotaObject::AddRef would try to acquire the same
// mutex.
const NotNull<CanonicalQuotaObject*> canonicalQuotaObject =
originInfo->mCanonicalQuotaObjects.LookupOrInsertWith(path, [&] {
// Create a new QuotaObject. The hashtable is not responsible to
// delete the QuotaObject.
return WrapNotNullUnchecked(new CanonicalQuotaObject(
originInfo, aClientType, path, fileSize));
});
// Addref the QuotaObject and move the ownership to the result. This must
// happen before we unlock!
result = canonicalQuotaObject->LockedAddRef();
}
if (aFileSizeOut) {
*aFileSizeOut = fileSize;
}
// The caller becomes the owner of the QuotaObject, that is, the caller is
// is responsible to delete it when the last reference is removed.
return result.forget();
}
already_AddRefed<QuotaObject> QuotaManager::GetQuotaObject(
PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata,
Client::Type aClientType, const nsAString& aPath, int64_t aFileSize,
int64_t* aFileSizeOut /* = nullptr */) {
NS_ASSERTION(!NS_IsMainThread(), "Wrong thread!");
if (aFileSizeOut) {
*aFileSizeOut = 0;
}
QM_TRY_INSPECT(const auto& file, QM_NewLocalFile(aPath), nullptr);
return GetQuotaObject(aPersistenceType, aOriginMetadata, aClientType, file,
aFileSize, aFileSizeOut);
}
already_AddRefed<QuotaObject> QuotaManager::GetQuotaObject(
const int64_t aDirectoryLockId, const nsAString& aPath) {
NS_ASSERTION(!NS_IsMainThread(), "Wrong thread!");
Maybe<MutexAutoLock> lock;
// See the comment for mDirectoryLockIdTable in QuotaManager.h
if (!IsOnBackgroundThread()) {
lock.emplace(mQuotaMutex);
}
if (auto maybeDirectoryLock =
mDirectoryLockIdTable.MaybeGet(aDirectoryLockId)) {
const auto& directoryLock = *maybeDirectoryLock;
MOZ_DIAGNOSTIC_ASSERT(directoryLock->ShouldUpdateLockIdTable());
const PersistenceType persistenceType = directoryLock->GetPersistenceType();
const OriginMetadata& originMetadata = directoryLock->OriginMetadata();
const Client::Type clientType = directoryLock->ClientType();
lock.reset();
return GetQuotaObject(persistenceType, originMetadata, clientType, aPath);
}
MOZ_ASSERT(aDirectoryLockId == -1);
return nullptr;
}
Nullable<bool> QuotaManager::OriginPersisted(
const OriginMetadata& aOriginMetadata) {
AssertIsOnIOThread();
MutexAutoLock lock(mQuotaMutex);
RefPtr<OriginInfo> originInfo =
LockedGetOriginInfo(PERSISTENCE_TYPE_DEFAULT, aOriginMetadata);
if (originInfo) {
return Nullable<bool>(originInfo->LockedPersisted());
}
return Nullable<bool>();
}
void QuotaManager::PersistOrigin(const OriginMetadata& aOriginMetadata) {
AssertIsOnIOThread();
MutexAutoLock lock(mQuotaMutex);
RefPtr<OriginInfo> originInfo =
LockedGetOriginInfo(PERSISTENCE_TYPE_DEFAULT, aOriginMetadata);
if (originInfo && !originInfo->LockedPersisted()) {
originInfo->LockedPersist();
}
}
void QuotaManager::AbortOperationsForLocks(
const DirectoryLockIdTableArray& aLockIds) {
for (Client::Type type : AllClientTypes()) {
if (aLockIds[type].Filled()) {
(*mClients)[type]->AbortOperationsForLocks(aLockIds[type]);
}
}
}
void QuotaManager::AbortOperationsForProcess(ContentParentId aContentParentId) {
AssertIsOnOwningThread();
for (const RefPtr<Client>& client : *mClients) {
client->AbortOperationsForProcess(aContentParentId);
}
}
Result<nsCOMPtr<nsIFile>, nsresult> QuotaManager::GetOriginDirectory(
const OriginMetadata& aOriginMetadata) const {
QM_TRY_UNWRAP(
auto directory,
QM_NewLocalFile(GetStoragePath(aOriginMetadata.mPersistenceType)));
QM_TRY(MOZ_TO_RESULT(directory->Append(
MakeSanitizedOriginString(aOriginMetadata.mStorageOrigin))));
return directory;
}
Result<bool, nsresult> QuotaManager::DoesOriginDirectoryExist(
const OriginMetadata& aOriginMetadata) const {
AssertIsOnIOThread();
QM_TRY_INSPECT(const auto& directory, GetOriginDirectory(aOriginMetadata));
QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(directory, Exists));
}
Result<nsCOMPtr<nsIFile>, nsresult>
QuotaManager::GetOrCreateTemporaryOriginDirectory(
const OriginMetadata& aOriginMetadata) {
AssertIsOnIOThread();
MOZ_ASSERT(aOriginMetadata.mPersistenceType != PERSISTENCE_TYPE_PERSISTENT);
MOZ_DIAGNOSTIC_ASSERT(IsStorageInitializedInternal());
MOZ_DIAGNOSTIC_ASSERT(IsTemporaryStorageInitializedInternal());
MOZ_ASSERT(IsTemporaryGroupInitializedInternal(aOriginMetadata));
MOZ_ASSERT(IsTemporaryOriginInitializedInternal(aOriginMetadata));
ScopedLogExtraInfo scope{
ScopedLogExtraInfo::kTagContextTainted,
"dom::quota::QuotaManager::GetOrCreateTemporaryOriginDirectory"_ns};
// XXX Temporary band-aid fix until the root cause of uninitialized origins
// after obtaining a client directory lock via OpenClientDirectory is
// identified.
QM_TRY(
// Expression.
MOZ_TO_RESULT(IsTemporaryOriginInitializedInternal(aOriginMetadata))
.mapErr([](const nsresult rv) { return NS_ERROR_NOT_INITIALIZED; }),
// Custom return value.
QM_PROPAGATE,
// Cleanup function.
([this, aOriginMetadata](const nsresult) {
MOZ_ALWAYS_SUCCEEDS(mOwningThread->Dispatch(
NS_NewRunnableFunction(
"QuotaManager::GetOrCreateTemporaryOriginDirectory",
[aOriginMetadata]() {
QuotaManager* quotaManager = QuotaManager::Get();
MOZ_ASSERT(quotaManager);
OriginMetadataArray originMetadataArray;
originMetadataArray.AppendElement(aOriginMetadata);
quotaManager->NoteUninitializedOrigins(originMetadataArray);
}),
NS_DISPATCH_NORMAL));
}));
QM_TRY_UNWRAP(auto directory, GetOriginDirectory(aOriginMetadata));
QM_TRY_INSPECT(const bool& created, EnsureOriginDirectory(*directory));
if (created) {
// A new origin directory has been created.
// We have a temporary origin which has been initialized without ensuring
// respective origin directory. So OriginInfo already exists and it needs
// to be updated because the origin directory has been just created.
auto [timestamp, persisted] =
WithOriginInfo(aOriginMetadata, [](const auto& originInfo) {
const int64_t timestamp = originInfo->LockedAccessTime();
const bool persisted = originInfo->LockedPersisted();
originInfo->LockedDirectoryCreated();
return std::make_pair(timestamp, persisted);
});
// Usually, infallible operations are placed after fallible ones. However,
// since we lack atomic support for creating the origin directory along
// with its metadata, we need to add the origin to cached origins right
// after directory creation.
AddTemporaryOrigin(
FullOriginMetadata{aOriginMetadata, persisted, timestamp});
QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata2(*directory, timestamp,
persisted, aOriginMetadata)));
}
return std::move(directory);
}
Result<Ok, nsresult> QuotaManager::EnsureTemporaryOriginDirectoryCreated(
const OriginMetadata& aOriginMetadata) {
QM_TRY_RETURN(GetOrCreateTemporaryOriginDirectory(aOriginMetadata)
.map([](const auto& res) { return Ok{}; }));
}
// static
nsresult QuotaManager::CreateDirectoryMetadata(
nsIFile& aDirectory, int64_t aTimestamp,
const OriginMetadata& aOriginMetadata) {
AssertIsOnIOThread();
StorageOriginAttributes groupAttributes;
nsCString groupNoSuffix;
QM_TRY(OkIf(groupAttributes.PopulateFromOrigin(aOriginMetadata.mGroup,
groupNoSuffix)),
NS_ERROR_FAILURE);
nsCString groupPrefix;
GetJarPrefix(groupAttributes.InIsolatedMozBrowser(), groupPrefix);
nsCString group = groupPrefix + groupNoSuffix;
StorageOriginAttributes originAttributes;
nsCString originNoSuffix;
QM_TRY(OkIf(originAttributes.PopulateFromOrigin(aOriginMetadata.mOrigin,
originNoSuffix)),
NS_ERROR_FAILURE);
nsCString originPrefix;
GetJarPrefix(originAttributes.InIsolatedMozBrowser(), originPrefix);
nsCString origin = originPrefix + originNoSuffix;
MOZ_ASSERT(groupPrefix == originPrefix);
QM_TRY_INSPECT(const auto& file, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<nsIFile>, aDirectory, Clone));
QM_TRY(MOZ_TO_RESULT(file->Append(nsLiteralString(METADATA_TMP_FILE_NAME))));
QM_TRY_INSPECT(const auto& stream,
GetBinaryOutputStream(*file, FileFlag::Truncate));
MOZ_ASSERT(stream);
QM_TRY(MOZ_TO_RESULT(stream->Write64(aTimestamp)));
QM_TRY(MOZ_TO_RESULT(stream->WriteStringZ(group.get())));
QM_TRY(MOZ_TO_RESULT(stream->WriteStringZ(origin.get())));
// Currently unused (used to be isApp).
QM_TRY(MOZ_TO_RESULT(stream->WriteBoolean(false)));
QM_TRY(MOZ_TO_RESULT(stream->Flush()));
QM_TRY(MOZ_TO_RESULT(stream->Close()));
QM_TRY(MOZ_TO_RESULT(
file->RenameTo(nullptr, nsLiteralString(METADATA_FILE_NAME))));
return NS_OK;
}
// static
nsresult QuotaManager::CreateDirectoryMetadata2(
nsIFile& aDirectory, int64_t aTimestamp, bool aPersisted,
const OriginMetadata& aOriginMetadata) {
AssertIsOnIOThread();
QM_TRY(ArtificialFailure(
nsIQuotaArtificialFailure::CATEGORY_CREATE_DIRECTORY_METADATA2));
QM_TRY_INSPECT(const auto& file, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<nsIFile>, aDirectory, Clone));
QM_TRY(
MOZ_TO_RESULT(file->Append(nsLiteralString(METADATA_V2_TMP_FILE_NAME))));
QM_TRY_INSPECT(const auto& stream,
GetBinaryOutputStream(*file, FileFlag::Truncate));
MOZ_ASSERT(stream);
QM_TRY(MOZ_TO_RESULT(stream->Write64(aTimestamp)));
QM_TRY(MOZ_TO_RESULT(stream->WriteBoolean(aPersisted)));
// Reserved data 1
QM_TRY(MOZ_TO_RESULT(stream->Write32(0)));
// Reserved data 2
QM_TRY(MOZ_TO_RESULT(stream->Write32(0)));
// Currently unused (used to be suffix).
QM_TRY(MOZ_TO_RESULT(stream->WriteStringZ("")));
// Currently unused (used to be group).
QM_TRY(MOZ_TO_RESULT(stream->WriteStringZ("")));
QM_TRY(MOZ_TO_RESULT(
stream->WriteStringZ(aOriginMetadata.mStorageOrigin.get())));
// Currently used for isPrivate (used to be used for isApp).
QM_TRY(MOZ_TO_RESULT(stream->WriteBoolean(aOriginMetadata.mIsPrivate)));
QM_TRY(MOZ_TO_RESULT(stream->Flush()));
QM_TRY(MOZ_TO_RESULT(stream->Close()));
QM_TRY(MOZ_TO_RESULT(
file->RenameTo(nullptr, nsLiteralString(METADATA_V2_FILE_NAME))));
return NS_OK;
}
nsresult QuotaManager::RestoreDirectoryMetadata2(nsIFile* aDirectory) {
AssertIsOnIOThread();
MOZ_ASSERT(aDirectory);
MOZ_ASSERT(mStorageConnection);
glean::quotamanager::restore_origin_directory_metadata_counter.Add();
RefPtr<RestoreDirectoryMetadata2Helper> helper =
new RestoreDirectoryMetadata2Helper(aDirectory);
QM_TRY(MOZ_TO_RESULT(helper->Init()));
QM_TRY(MOZ_TO_RESULT(helper->RestoreMetadata2File()));
return NS_OK;
}
Result<FullOriginMetadata, nsresult> QuotaManager::LoadFullOriginMetadata(
nsIFile* aDirectory, PersistenceType aPersistenceType) {
MOZ_ASSERT(!NS_IsMainThread());
MOZ_ASSERT(aDirectory);
MOZ_ASSERT(mStorageConnection);
QM_TRY_INSPECT(const auto& binaryStream,
GetBinaryInputStream(*aDirectory,
nsLiteralString(METADATA_V2_FILE_NAME)));
FullOriginMetadata fullOriginMetadata;
QM_TRY_UNWRAP(fullOriginMetadata.mLastAccessTime,
MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, Read64));
QM_TRY_UNWRAP(fullOriginMetadata.mPersisted,
MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, ReadBoolean));
QM_TRY_INSPECT(const bool& reservedData1,
MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, Read32));
Unused << reservedData1;
// XXX Use for the persistence type.
QM_TRY_INSPECT(const bool& reservedData2,
MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, Read32));
Unused << reservedData2;
fullOriginMetadata.mPersistenceType = aPersistenceType;
QM_TRY_INSPECT(const auto& suffix, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCString, binaryStream, ReadCString));
Unused << suffix;
QM_TRY_INSPECT(const auto& group, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCString, binaryStream, ReadCString));
Unused << group;
QM_TRY_UNWRAP(
fullOriginMetadata.mStorageOrigin,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, binaryStream, ReadCString));
// Currently used for isPrivate (used to be used for isApp).
QM_TRY_UNWRAP(fullOriginMetadata.mIsPrivate,
MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, ReadBoolean));
QM_TRY(MOZ_TO_RESULT(binaryStream->Close()));
auto principal =
[&storageOrigin =
fullOriginMetadata.mStorageOrigin]() -> nsCOMPtr<nsIPrincipal> {
if (storageOrigin.EqualsLiteral(kChromeOrigin)) {
return SystemPrincipal::Get();
}
return BasePrincipal::CreateContentPrincipal(storageOrigin);
}();
QM_TRY(MOZ_TO_RESULT(principal));
PrincipalInfo principalInfo;
QM_TRY(MOZ_TO_RESULT(PrincipalToPrincipalInfo(principal, &principalInfo)));
QM_TRY(MOZ_TO_RESULT(IsPrincipalInfoValid(principalInfo)),
Err(NS_ERROR_MALFORMED_URI));
QM_TRY_UNWRAP(auto principalMetadata,
GetInfoFromValidatedPrincipalInfo(*this, principalInfo));
fullOriginMetadata.mSuffix = std::move(principalMetadata.mSuffix);
fullOriginMetadata.mGroup = std::move(principalMetadata.mGroup);
fullOriginMetadata.mOrigin = std::move(principalMetadata.mOrigin);
QM_TRY_INSPECT(const bool& groupUpdated,
MaybeUpdateGroupForOrigin(fullOriginMetadata));
// A workaround for a bug in GetLastModifiedTime implementation which should
// have returned the current time instead of INT64_MIN when there were no
// suitable files for getting last modified time.
QM_TRY_INSPECT(const bool& lastAccessTimeUpdated,
MaybeUpdateLastAccessTimeForOrigin(fullOriginMetadata));
if (groupUpdated || lastAccessTimeUpdated) {
// Only overwriting .metadata-v2 (used to overwrite .metadata too) to reduce
// I/O.
QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata2(
*aDirectory, fullOriginMetadata.mLastAccessTime,
fullOriginMetadata.mPersisted, fullOriginMetadata)));
}
return fullOriginMetadata;
}
Result<FullOriginMetadata, nsresult>
QuotaManager::LoadFullOriginMetadataWithRestore(nsIFile* aDirectory) {
// XXX Once the persistence type is stored in the metadata file, this block
// for getting the persistence type from the parent directory name can be
// removed.
nsCOMPtr<nsIFile> parentDir;
QM_TRY(MOZ_TO_RESULT(aDirectory->GetParent(getter_AddRefs(parentDir))));
const auto maybePersistenceType =
PersistenceTypeFromFile(*parentDir, fallible);
QM_TRY(OkIf(maybePersistenceType.isSome()), Err(NS_ERROR_FAILURE));
const auto& persistenceType = maybePersistenceType.value();
QM_TRY_RETURN(QM_OR_ELSE_WARN(
// Expression.
LoadFullOriginMetadata(aDirectory, persistenceType),
// Fallback.
([&aDirectory, &persistenceType,
this](const nsresult rv) -> Result<FullOriginMetadata, nsresult> {
QM_TRY(MOZ_TO_RESULT(RestoreDirectoryMetadata2(aDirectory)));
QM_TRY_RETURN(LoadFullOriginMetadata(aDirectory, persistenceType));
})));
}
Result<OriginMetadata, nsresult> QuotaManager::GetOriginMetadata(
nsIFile* aDirectory) {
MOZ_ASSERT(aDirectory);
MOZ_ASSERT(mStorageConnection);
QM_TRY_INSPECT(
const auto& leafName,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoString, aDirectory, GetLeafName));
// XXX Consider using QuotaManager::ParseOrigin here.
nsCString spec;
OriginAttributes attrs;
nsCString originalSuffix;
OriginParser::ResultType result = OriginParser::ParseOrigin(
NS_ConvertUTF16toUTF8(leafName), spec, &attrs, originalSuffix);
QM_TRY(MOZ_TO_RESULT(result == OriginParser::ValidOrigin));
QM_TRY_INSPECT(
const auto& principal,
([&spec, &attrs]() -> Result<nsCOMPtr<nsIPrincipal>, nsresult> {
if (spec.EqualsLiteral(kChromeOrigin)) {
return nsCOMPtr<nsIPrincipal>(SystemPrincipal::Get());
}
nsCOMPtr<nsIURI> uri;
QM_TRY(MOZ_TO_RESULT(NS_NewURI(getter_AddRefs(uri), spec)));
return nsCOMPtr<nsIPrincipal>(
BasePrincipal::CreateContentPrincipal(uri, attrs));
}()));
QM_TRY(MOZ_TO_RESULT(principal));
PrincipalInfo principalInfo;
QM_TRY(MOZ_TO_RESULT(PrincipalToPrincipalInfo(principal, &principalInfo)));
QM_TRY(MOZ_TO_RESULT(IsPrincipalInfoValid(principalInfo)),
Err(NS_ERROR_MALFORMED_URI));
QM_TRY_UNWRAP(auto principalMetadata,
GetInfoFromValidatedPrincipalInfo(*this, principalInfo));
QM_TRY_INSPECT(const auto& parentDirectory,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCOMPtr<nsIFile>,
aDirectory, GetParent));
const auto maybePersistenceType =
PersistenceTypeFromFile(*parentDirectory, fallible);
QM_TRY(MOZ_TO_RESULT(maybePersistenceType.isSome()));
return OriginMetadata{std::move(principalMetadata),
maybePersistenceType.value()};
}
Result<Ok, nsresult> QuotaManager::RemoveOriginDirectory(nsIFile& aDirectory) {
AssertIsOnIOThread();
if (!AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownTeardown)) {
QM_TRY_RETURN(MOZ_TO_RESULT(aDirectory.Remove(true)));
}
QM_TRY_INSPECT(const auto& toBeRemovedStorageDir,
QM_NewLocalFile(*mToBeRemovedStoragePath));
QM_TRY_INSPECT(const bool& created, EnsureDirectory(*toBeRemovedStorageDir));
(void)created;
QM_TRY_RETURN(MOZ_TO_RESULT(aDirectory.MoveTo(
toBeRemovedStorageDir, NSID_TrimBracketsUTF16(nsID::GenerateUUID()))));
}
Result<bool, nsresult> QuotaManager::DoesClientDirectoryExist(
const ClientMetadata& aClientMetadata) const {
AssertIsOnIOThread();
QM_TRY_INSPECT(const auto& directory, GetOriginDirectory(aClientMetadata));
QM_TRY(MOZ_TO_RESULT(
directory->Append(Client::TypeToString(aClientMetadata.mClientType))));
QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(directory, Exists));
}
template <typename OriginFunc>
nsresult QuotaManager::InitializeRepository(PersistenceType aPersistenceType,
OriginFunc&& aOriginFunc) {
AssertIsOnIOThread();
MOZ_ASSERT(aPersistenceType == PERSISTENCE_TYPE_PERSISTENT ||
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;
uint64_t iterations = 0;
// 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, &iterations, &childDirectory, &renameAndInitInfos,
aPersistenceType, &aOriginFunc]() -> Result<Ok, nsresult> {
QM_TRY_INSPECT(
const auto& leafName,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsAutoString, childDirectory, GetLeafName));
QM_TRY_INSPECT(const auto& dirEntryKind,
GetDirEntryKind(*childDirectory));
switch (dirEntryKind) {
case nsIFileKind::ExistsAsDirectory: {
QM_TRY_UNWRAP(
auto maybeMetadata,
QM_OR_ELSE_WARN_IF(
// Expression
LoadFullOriginMetadataWithRestore(
childDirectory)
.