From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- dom/cache/QuotaClient.cpp | 548 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 548 insertions(+) create mode 100644 dom/cache/QuotaClient.cpp (limited to 'dom/cache/QuotaClient.cpp') diff --git a/dom/cache/QuotaClient.cpp b/dom/cache/QuotaClient.cpp new file mode 100644 index 0000000000..64bb501385 --- /dev/null +++ b/dom/cache/QuotaClient.cpp @@ -0,0 +1,548 @@ +/* -*- 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 "QuotaClientImpl.h" + +#include "DBAction.h" +#include "FileUtilsImpl.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/Telemetry.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/cache/DBSchema.h" +#include "mozilla/dom/cache/Manager.h" +#include "mozilla/dom/quota/PersistenceType.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozilla/dom/quota/QuotaManager.h" +#include "mozilla/dom/quota/UsageInfo.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "nsIFile.h" +#include "nsThreadUtils.h" + +namespace mozilla::dom::cache { + +using mozilla::dom::quota::AssertIsOnIOThread; +using mozilla::dom::quota::Client; +using mozilla::dom::quota::CloneFileAndAppend; +using mozilla::dom::quota::DatabaseUsageType; +using mozilla::dom::quota::GetDirEntryKind; +using mozilla::dom::quota::nsIFileKind; +using mozilla::dom::quota::OriginMetadata; +using mozilla::dom::quota::PrincipalMetadata; +using mozilla::dom::quota::QuotaManager; +using mozilla::dom::quota::UsageInfo; +using mozilla::ipc::AssertIsOnBackgroundThread; + +namespace { + +template +Result ReduceUsageInfo(nsIFile& aDir, + const Atomic& aCanceled, + const StepFunc& aStepFunc) { + QM_TRY_RETURN(quota::ReduceEachFileAtomicCancelable( + aDir, aCanceled, UsageInfo{}, + [&aStepFunc](UsageInfo usageInfo, const nsCOMPtr& bodyDir) + -> Result { + QM_TRY(OkIf(!QuotaManager::IsShuttingDown()).mapErr([](const auto&) { + return NS_ERROR_ABORT; + })); + + QM_TRY_INSPECT(const auto& stepUsageInfo, aStepFunc(bodyDir)); + + return usageInfo + stepUsageInfo; + })); +} + +Result GetPaddingSizeFromDB( + nsIFile& aDir, nsIFile& aDBFile, const OriginMetadata& aOriginMetadata, + const Maybe& aMaybeCipherKey) { + CacheDirectoryMetadata directoryMetadata(aOriginMetadata); + // directoryMetadata.mDirectoryLockId must be -1 (which is default for new + // CacheDirectoryMetadata) because this method should only be called from + // QuotaClient::InitOrigin when the temporary storage hasn't been initialized + // yet. At that time, the in-memory objects (e.g. OriginInfo) are only being + // created so it doesn't make sense to tunnel quota information to QuotaVFS + // to get corresponding QuotaObject instance for the SQLite file). + MOZ_DIAGNOSTIC_ASSERT(directoryMetadata.mDirectoryLockId == -1); + +#ifdef DEBUG + { + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(aDBFile, Exists)); + MOZ_ASSERT(exists); + } +#endif + + QM_TRY_INSPECT(const auto& conn, + OpenDBConnection(directoryMetadata, aDBFile, aMaybeCipherKey)); + + // Make sure that the database has the latest schema before we try to read + // from it. We have to do this because GetPaddingSizeFromDB is called + // by InitOrigin. And it means that SetupAction::RunSyncWithDBOnTarget hasn't + // checked the schema for the given origin yet). + QM_TRY(MOZ_TO_RESULT(db::CreateOrMigrateSchema(aDir, *conn))); + + QM_TRY_RETURN(DirectoryPaddingRestore(aDir, *conn, + /* aMustRestore */ false)); +} + +Result GetTotalDiskUsageFromDB( + nsIFile& aDir, nsIFile& aDBFile, const OriginMetadata& aOriginMetadata, + const Maybe& aMaybeCipherKey) { + CacheDirectoryMetadata directoryMetadata(aOriginMetadata); + // directoryMetadata.mDirectoryLockId must be -1 (which is default for new + // CacheDirectoryMetadata) because this method should only be called from + // QuotaClient::InitOrigin when the temporary storage hasn't been initialized + // yet. At that time, the in-memory objects (e.g. OriginInfo) are only being + // created so it doesn't make sense to tunnel quota information to QuotaVFS + // to get corresponding QuotaObject instance for the SQLite file). + MOZ_DIAGNOSTIC_ASSERT(directoryMetadata.mDirectoryLockId == -1); + +#ifdef DEBUG + { + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(aDBFile, Exists)); + MOZ_ASSERT(exists); + } +#endif + + QM_TRY_INSPECT(const auto& conn, + OpenDBConnection(directoryMetadata, aDBFile, aMaybeCipherKey)); + + // Make sure that the database has the latest schema before we try to read + // from it. We have to do this because GetTotalDiskUsageFromDB is called + // by InitOrigin. And it means that SetupAction::RunSyncWithDBOnTarget hasn't + // checked the schema for the given origin yet). + QM_TRY(MOZ_TO_RESULT(db::CreateOrMigrateSchema(aDir, *conn))); + + QM_TRY_RETURN(db::GetTotalDiskUsage(*conn)); +} + +} // namespace + +const nsLiteralString kCachesSQLiteFilename = u"caches.sqlite"_ns; +const nsLiteralString kMorgueDirectoryFilename = u"morgue"_ns; + +CacheQuotaClient::CacheQuotaClient() { + AssertIsOnBackgroundThread(); + MOZ_DIAGNOSTIC_ASSERT(!sInstance); + sInstance = this; +} + +// static +CacheQuotaClient* CacheQuotaClient::Get() { + MOZ_DIAGNOSTIC_ASSERT(sInstance); + return sInstance; +} + +CacheQuotaClient::Type CacheQuotaClient::GetType() { return DOMCACHE; } + +Result CacheQuotaClient::InitOrigin( + PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, + const AtomicBool& aCanceled) { + AssertIsOnIOThread(); + MOZ_ASSERT(aOriginMetadata.mPersistenceType == aPersistenceType); + + QuotaManager* const qm = QuotaManager::Get(); + MOZ_DIAGNOSTIC_ASSERT(qm); + + QM_TRY_INSPECT(const auto& dir, qm->GetOriginDirectory(aOriginMetadata)); + + QM_TRY(MOZ_TO_RESULT( + dir->Append(NS_LITERAL_STRING_FROM_CSTRING(DOMCACHE_DIRECTORY_NAME)))); + + QM_TRY_INSPECT( + const auto& cachesSQLiteFile, + ([dir]() -> Result, nsresult> { + QM_TRY_INSPECT(const auto& cachesSQLite, + CloneFileAndAppend(*dir, kCachesSQLiteFilename)); + + // IsDirectory is used to check if caches.sqlite exists or not. Another + // benefit of this is that we can test the failed cases by creating a + // directory named "caches.sqlite". + QM_TRY_INSPECT(const auto& dirEntryKind, + GetDirEntryKind(*cachesSQLite)); + if (dirEntryKind == nsIFileKind::DoesNotExist) { + // We only ensure padding files and morgue directory get removed like + // WipeDatabase in DBAction.cpp. The -wal journal file will be + // automatically deleted by sqlite when the new database is created. + // XXX Ideally, we would delete the -wal journal file as well (here + // and also in WipeDatabase). + // XXX We should have something like WipeDatabaseNoQuota for this. + // XXX Long term, we might even think about removing entire origin + // directory because missing caches.sqlite while other files exist can + // be interpreted as database corruption. + QM_TRY(MOZ_TO_RESULT(mozilla::dom::cache::DirectoryPaddingDeleteFile( + *dir, DirPaddingFile::TMP_FILE))); + + QM_TRY(MOZ_TO_RESULT(mozilla::dom::cache::DirectoryPaddingDeleteFile( + *dir, DirPaddingFile::FILE))); + + QM_TRY_INSPECT(const auto& morgueDir, + CloneFileAndAppend(*dir, kMorgueDirectoryFilename)); + + QM_TRY(MOZ_TO_RESULT(mozilla::dom::cache::RemoveNsIFileRecursively( + Nothing(), *morgueDir, + /* aTrackQuota */ false))); + + return nsCOMPtr{nullptr}; + } + + QM_TRY(OkIf(dirEntryKind == nsIFileKind::ExistsAsFile), + Err(NS_ERROR_FAILURE)); + + return cachesSQLite; + }())); + + // If the caches.sqlite doesn't exist, then padding files and morgue directory + // should have been removed if they existed. We ignore the rest of known files + // because we assume that they will be removed when a new database is created. + // XXX Ensure the -wel file is removed if the caches.sqlite doesn't exist. + QM_TRY(OkIf(!!cachesSQLiteFile), UsageInfo{}); + + const auto maybeCipherKey = [this, &aOriginMetadata] { + Maybe maybeCipherKey; + auto cipherKeyManager = GetOrCreateCipherKeyManager(aOriginMetadata); + if (cipherKeyManager) { + maybeCipherKey = Some(cipherKeyManager->Ensure()); + } + return maybeCipherKey; + }(); + + QM_TRY_INSPECT( + const auto& paddingSize, + ([dir, cachesSQLiteFile, &aOriginMetadata, + &maybeCipherKey]() -> Result { + if (!DirectoryPaddingFileExists(*dir, DirPaddingFile::TMP_FILE)) { + QM_WARNONLY_TRY_UNWRAP(const auto maybePaddingSize, + DirectoryPaddingGet(*dir)); + if (maybePaddingSize) { + return maybePaddingSize.ref(); + } + } + + // If the temporary file still exists or failing to get the padding size + // from the padding file, then we need to get the padding size from the + // database and restore the padding file. + QM_TRY_RETURN(GetPaddingSizeFromDB(*dir, *cachesSQLiteFile, + aOriginMetadata, maybeCipherKey)); + }())); + + QM_TRY_INSPECT(const auto& totalDiskUsage, + GetTotalDiskUsageFromDB(*dir, *cachesSQLiteFile, + aOriginMetadata, maybeCipherKey)); + + QM_TRY_INSPECT( + const auto& innerUsageInfo, + ReduceUsageInfo( + *dir, aCanceled, + [](const nsCOMPtr& file) -> Result { + QM_TRY_INSPECT(const auto& leafName, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoString, file, + GetLeafName)); + + QM_TRY_INSPECT(const auto& dirEntryKind, GetDirEntryKind(*file)); + + switch (dirEntryKind) { + case nsIFileKind::ExistsAsDirectory: + if (!leafName.EqualsLiteral("morgue")) { + NS_WARNING("Unknown Cache directory found!"); + } + + break; + + case nsIFileKind::ExistsAsFile: + // Ignore transient sqlite files and marker files + if (leafName.EqualsLiteral("caches.sqlite-journal") || + leafName.EqualsLiteral("caches.sqlite-shm") || + StringBeginsWith(leafName, u"caches.sqlite-mj"_ns) || + leafName.EqualsLiteral("context_open.marker")) { + break; + } + + if (leafName.Equals(kCachesSQLiteFilename) || + leafName.EqualsLiteral("caches.sqlite-wal")) { + QM_TRY_INSPECT( + const int64_t& fileSize, + MOZ_TO_RESULT_INVOKE_MEMBER(file, GetFileSize)); + MOZ_DIAGNOSTIC_ASSERT(fileSize >= 0); + + return UsageInfo{DatabaseUsageType(Some(fileSize))}; + } + + // Ignore directory padding file + if (leafName.EqualsLiteral(PADDING_FILE_NAME) || + leafName.EqualsLiteral(PADDING_TMP_FILE_NAME)) { + break; + } + + NS_WARNING("Unknown Cache file found!"); + + break; + + case nsIFileKind::DoesNotExist: + // Ignore files that got removed externally while iterating. + break; + } + + return UsageInfo{}; + })); + + // FIXME: Separate file usage and database usage in OriginInfo so that the + // workaround for treating padding file size as database usage can be removed. + return UsageInfo{DatabaseUsageType(Some(paddingSize))} + + UsageInfo{DatabaseUsageType(Some(totalDiskUsage))} + innerUsageInfo; +} + +nsresult CacheQuotaClient::InitOriginWithoutTracking( + PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, + const AtomicBool& aCanceled) { + AssertIsOnIOThread(); + + // This is called when a storage/permanent/${origin}/cache directory exists. + // Even though this shouldn't happen with a "good" profile, we shouldn't + // return an error here, since that would cause origin initialization to fail. + // We just warn and otherwise ignore that. + UNKNOWN_FILE_WARNING(NS_LITERAL_STRING_FROM_CSTRING(DOMCACHE_DIRECTORY_NAME)); + return NS_OK; +} + +Result CacheQuotaClient::GetUsageForOrigin( + PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, + const AtomicBool& aCanceled) { + AssertIsOnIOThread(); + + // We can't open the database at this point, since it can be already used by + // the Cache IO thread. Use the cached value instead. + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + return quotaManager->GetUsageForClient(aOriginMetadata.mPersistenceType, + aOriginMetadata, Client::DOMCACHE); +} + +void CacheQuotaClient::OnOriginClearCompleted(PersistenceType aPersistenceType, + const nsACString& aOrigin) { + AssertIsOnIOThread(); + + if (aPersistenceType == quota::PERSISTENCE_TYPE_PRIVATE) { + if (auto entry = mCipherKeyManagers.Lookup(aOrigin)) { + entry.Data()->Invalidate(); + entry.Remove(); + } + } +} + +void CacheQuotaClient::OnRepositoryClearCompleted( + PersistenceType aPersistenceType) { + AssertIsOnIOThread(); + + if (aPersistenceType == quota::PERSISTENCE_TYPE_PRIVATE) { + for (const auto& cipherKeyManager : mCipherKeyManagers.Values()) { + cipherKeyManager->Invalidate(); + } + + mCipherKeyManagers.Clear(); + } +} + +void CacheQuotaClient::ReleaseIOThreadObjects() { + // Nothing to do here as the Context handles cleaning everything up + // automatically. +} + +void CacheQuotaClient::AbortOperationsForLocks( + const DirectoryLockIdTable& aDirectoryLockIds) { + AssertIsOnBackgroundThread(); + + Manager::Abort(aDirectoryLockIds); +} + +void CacheQuotaClient::AbortOperationsForProcess( + ContentParentId aContentParentId) { + // The Cache and Context can be shared by multiple client processes. They + // are not exclusively owned by a single process. + // + // As far as I can tell this is used by QuotaManager to abort operations + // when a particular process goes away. We definitely don't want this + // since we are shared. Also, the Cache actor code already properly + // handles asynchronous actor destruction when the child process dies. + // + // Therefore, do nothing here. +} + +void CacheQuotaClient::AbortAllOperations() { + AssertIsOnBackgroundThread(); + + Manager::AbortAll(); +} + +void CacheQuotaClient::StartIdleMaintenance() {} + +void CacheQuotaClient::StopIdleMaintenance() {} + +void CacheQuotaClient::InitiateShutdown() { + AssertIsOnBackgroundThread(); + + Manager::InitiateShutdown(); +} + +bool CacheQuotaClient::IsShutdownCompleted() const { + AssertIsOnBackgroundThread(); + + return Manager::IsShutdownAllComplete(); +} + +void CacheQuotaClient::ForceKillActors() { + // Currently we don't implement killing actors (are there any to kill here?). +} + +nsCString CacheQuotaClient::GetShutdownStatus() const { + AssertIsOnBackgroundThread(); + + return Manager::GetShutdownStatus(); +} + +void CacheQuotaClient::FinalizeShutdown() { + // Nothing to do here. +} + +nsresult CacheQuotaClient::UpgradeStorageFrom2_0To2_1(nsIFile* aDirectory) { + AssertIsOnIOThread(); + MOZ_DIAGNOSTIC_ASSERT(aDirectory); + + QM_TRY(MOZ_TO_RESULT(DirectoryPaddingInit(*aDirectory))); + + return NS_OK; +} + +nsresult CacheQuotaClient::RestorePaddingFileInternal( + nsIFile* aBaseDir, mozIStorageConnection* aConn) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(aBaseDir); + MOZ_DIAGNOSTIC_ASSERT(aConn); + + QM_TRY_INSPECT(const int64_t& dummyPaddingSize, + DirectoryPaddingRestore(*aBaseDir, *aConn, + /* aMustRestore */ true)); + Unused << dummyPaddingSize; + + return NS_OK; +} + +nsresult CacheQuotaClient::WipePaddingFileInternal( + const CacheDirectoryMetadata& aDirectoryMetadata, nsIFile* aBaseDir) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(aBaseDir); + + MOZ_ASSERT(DirectoryPaddingFileExists(*aBaseDir, DirPaddingFile::FILE)); + + QM_TRY_INSPECT( + const int64_t& paddingSize, ([&aBaseDir]() -> Result { + const bool temporaryPaddingFileExist = + DirectoryPaddingFileExists(*aBaseDir, DirPaddingFile::TMP_FILE); + + Maybe directoryPaddingGetResult; + if (!temporaryPaddingFileExist) { + QM_TRY_UNWRAP(directoryPaddingGetResult, + ([&aBaseDir]() -> Result, nsresult> { + QM_TRY_RETURN( + DirectoryPaddingGet(*aBaseDir).map(Some), + Maybe{}); + }())); + } + + if (temporaryPaddingFileExist || !directoryPaddingGetResult) { + // XXXtt: Maybe have a method in the QuotaManager to clean the usage + // under the quota client and the origin. There is nothing we can do + // to recover the file. + NS_WARNING("Cannnot read padding size from file!"); + return 0; + } + + return *directoryPaddingGetResult; + }())); + + if (paddingSize > 0) { + DecreaseUsageForDirectoryMetadata(aDirectoryMetadata, paddingSize); + } + + QM_TRY(MOZ_TO_RESULT( + DirectoryPaddingDeleteFile(*aBaseDir, DirPaddingFile::FILE))); + + // Remove temporary file if we have one. + QM_TRY(MOZ_TO_RESULT( + DirectoryPaddingDeleteFile(*aBaseDir, DirPaddingFile::TMP_FILE))); + + QM_TRY(MOZ_TO_RESULT(DirectoryPaddingInit(*aBaseDir))); + + return NS_OK; +} + +RefPtr CacheQuotaClient::GetOrCreateCipherKeyManager( + const PrincipalMetadata& aMetadata) { + AssertIsOnIOThread(); + + auto privateOrigin = aMetadata.mIsPrivate; + if (!privateOrigin) { + return nullptr; + } + + const auto& origin = aMetadata.mOrigin; + return mCipherKeyManagers.LookupOrInsertWith( + origin, [] { return new CipherKeyManager("CacheCipherKeyManager"); }); +} + +CacheQuotaClient::~CacheQuotaClient() { + AssertIsOnBackgroundThread(); + MOZ_DIAGNOSTIC_ASSERT(sInstance == this); + + sInstance = nullptr; +} + +// static +CacheQuotaClient* CacheQuotaClient::sInstance = nullptr; + +// static +already_AddRefed CreateQuotaClient() { + AssertIsOnBackgroundThread(); + + RefPtr ref = new CacheQuotaClient(); + return ref.forget(); +} + +// static +nsresult RestorePaddingFile(nsIFile* aBaseDir, mozIStorageConnection* aConn) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(aBaseDir); + MOZ_DIAGNOSTIC_ASSERT(aConn); + + RefPtr cacheQuotaClient = CacheQuotaClient::Get(); + MOZ_DIAGNOSTIC_ASSERT(cacheQuotaClient); + + QM_TRY(MOZ_TO_RESULT( + cacheQuotaClient->RestorePaddingFileInternal(aBaseDir, aConn))); + + return NS_OK; +} + +// static +nsresult WipePaddingFile(const CacheDirectoryMetadata& aDirectoryMetadata, + nsIFile* aBaseDir) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(aBaseDir); + + RefPtr cacheQuotaClient = CacheQuotaClient::Get(); + MOZ_DIAGNOSTIC_ASSERT(cacheQuotaClient); + + QM_TRY(MOZ_TO_RESULT( + cacheQuotaClient->WipePaddingFileInternal(aDirectoryMetadata, aBaseDir))); + + return NS_OK; +} + +} // namespace mozilla::dom::cache -- cgit v1.2.3