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