/* -*- 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 "FileSystemDataManager.h" #include "ErrorList.h" #include "FileSystemDatabaseManager.h" #include "FileSystemDatabaseManagerVersion001.h" #include "FileSystemDatabaseManagerVersion002.h" #include "FileSystemFileManager.h" #include "FileSystemHashSource.h" #include "FileSystemParentTypes.h" #include "ResultStatement.h" #include "SchemaVersion001.h" #include "SchemaVersion002.h" #include "fs/FileSystemConstants.h" #include "mozIStorageService.h" #include "mozStorageCID.h" #include "mozilla/Result.h" #include "mozilla/StaticPtr.h" #include "mozilla/dom/FileSystemLog.h" #include "mozilla/dom/FileSystemManagerParent.h" #include "mozilla/dom/QMResult.h" #include "mozilla/dom/quota/ClientImpl.h" #include "mozilla/dom/quota/DirectoryLock.h" #include "mozilla/dom/quota/QuotaCommon.h" #include "mozilla/dom/quota/QuotaManager.h" #include "mozilla/dom/quota/ResultExtensions.h" #include "mozilla/dom/quota/UsageInfo.h" #include "mozilla/ipc/BackgroundParent.h" #include "nsBaseHashtable.h" #include "nsCOMPtr.h" #include "nsHashKeys.h" #include "nsIFile.h" #include "nsIFileURL.h" #include "nsNetCID.h" #include "nsServiceManagerUtils.h" #include "nsString.h" #include "nsThreadUtils.h" namespace mozilla::dom::fs::data { namespace { // nsCStringHashKey with disabled memmove class nsCStringHashKeyDM : public nsCStringHashKey { public: explicit nsCStringHashKeyDM(const nsCStringHashKey::KeyTypePointer aKey) : nsCStringHashKey(aKey) {} enum { ALLOW_MEMMOVE = false }; }; // When CheckedUnsafePtr's checking is enabled, it's necessary to ensure that // the hashtable uses the copy constructor instead of memmove for moving entries // since memmove will break CheckedUnsafePtr in a memory-corrupting way. using FileSystemDataManagerHashKey = std::conditional::type; // Raw (but checked when the diagnostic assert is enabled) references as we // don't want to keep FileSystemDataManager objects alive forever. When a // FileSystemDataManager is destroyed it calls RemoveFileSystemDataManager // to clear itself. using FileSystemDataManagerHashtable = nsBaseHashtable>, MovingNotNull>>; // This hashtable isn't protected by any mutex but it is only ever touched on // the PBackground thread. StaticAutoPtr gDataManagers; RefPtr GetFileSystemDataManager(const Origin& aOrigin) { ::mozilla::ipc::AssertIsOnBackgroundThread(); if (gDataManagers) { auto maybeDataManager = gDataManagers->MaybeGet(aOrigin); if (maybeDataManager) { RefPtr result( std::move(*maybeDataManager).unwrapBasePtr()); return result; } } return nullptr; } void AddFileSystemDataManager( const Origin& aOrigin, const RefPtr& aDataManager) { ::mozilla::ipc::AssertIsOnBackgroundThread(); MOZ_ASSERT(!quota::QuotaManager::IsShuttingDown()); if (!gDataManagers) { gDataManagers = new FileSystemDataManagerHashtable(); } MOZ_ASSERT(!gDataManagers->Contains(aOrigin)); gDataManagers->InsertOrUpdate(aOrigin, WrapMovingNotNullUnchecked(aDataManager)); } void RemoveFileSystemDataManager(const Origin& aOrigin) { ::mozilla::ipc::AssertIsOnBackgroundThread(); MOZ_ASSERT(gDataManagers); const DebugOnly removed = gDataManagers->Remove(aOrigin); MOZ_ASSERT(removed); if (!gDataManagers->Count()) { gDataManagers = nullptr; } } } // namespace Result GetStorageConnection( const quota::OriginMetadata& aOriginMetadata, const int64_t aDirectoryLockId) { MOZ_ASSERT(aDirectoryLockId >= -1); // Ensure that storage is initialized and file system folder exists! QM_TRY_INSPECT(const auto& dbFileUrl, GetDatabaseFileURL(aOriginMetadata, aDirectoryLockId)); QM_TRY_INSPECT( const auto& storageService, QM_TO_RESULT_TRANSFORM(MOZ_TO_RESULT_GET_TYPED( nsCOMPtr, MOZ_SELECT_OVERLOAD(do_GetService), MOZ_STORAGE_SERVICE_CONTRACTID))); QM_TRY_UNWRAP(auto connection, QM_TO_RESULT_TRANSFORM(MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( nsCOMPtr, storageService, OpenDatabaseWithFileURL, dbFileUrl, ""_ns, mozIStorageService::CONNECTION_DEFAULT))); ResultConnection result(connection); return result; } Result GetRootHandle(const Origin& origin) { MOZ_ASSERT(!origin.IsEmpty()); return FileSystemHashSource::GenerateHash(origin, kRootString); } Result GetEntryHandle( const FileSystemChildMetadata& aHandle) { MOZ_ASSERT(!aHandle.parentId().IsEmpty()); return FileSystemHashSource::GenerateHash(aHandle.parentId(), aHandle.childName()); } FileSystemDataManager::FileSystemDataManager( const quota::OriginMetadata& aOriginMetadata, RefPtr aQuotaManager, MovingNotNull> aIOTarget, MovingNotNull> aIOTaskQueue) : mOriginMetadata(aOriginMetadata), mQuotaManager(std::move(aQuotaManager)), mBackgroundTarget(WrapNotNull(GetCurrentSerialEventTarget())), mIOTarget(std::move(aIOTarget)), mIOTaskQueue(std::move(aIOTaskQueue)), mRegCount(0), mVersion(0), mState(State::Initial) {} FileSystemDataManager::~FileSystemDataManager() { NS_ASSERT_OWNINGTHREAD(FileSystemDataManager); MOZ_ASSERT(mState == State::Closed); MOZ_ASSERT(!mDatabaseManager); } RefPtr FileSystemDataManager::GetOrCreateFileSystemDataManager( const quota::OriginMetadata& aOriginMetadata) { if (quota::QuotaManager::IsShuttingDown()) { return CreatePromise::CreateAndReject(NS_ERROR_FAILURE, __func__); } if (RefPtr dataManager = GetFileSystemDataManager(aOriginMetadata.mOrigin)) { if (dataManager->IsOpening()) { // We have to wait for the open to be finished before resolving the // promise. The manager can't close itself in the meantime because we // add a new registration in the lambda capture list. return dataManager->OnOpen()->Then( GetCurrentSerialEventTarget(), __func__, [dataManager = Registered(dataManager)]( const BoolPromise::ResolveOrRejectValue& aValue) { if (aValue.IsReject()) { return CreatePromise::CreateAndReject(aValue.RejectValue(), __func__); } return CreatePromise::CreateAndResolve(dataManager, __func__); }); } if (dataManager->IsClosing()) { // First, we need to wait for the close to be finished. After that the // manager is closed and it can't be opened again. The only option is // to create a new manager and open it. However, all this stuff is // asynchronous, so it can happen that something else called // `GetOrCreateFileSystemManager` in the meantime. For that reason, we // shouldn't try to create a new manager and open it here, a "recursive" // call to `GetOrCreateFileSystemManager` will handle any new situation. return dataManager->OnClose()->Then( GetCurrentSerialEventTarget(), __func__, [aOriginMetadata](const BoolPromise::ResolveOrRejectValue&) { return GetOrCreateFileSystemDataManager(aOriginMetadata); }); } return CreatePromise::CreateAndResolve( Registered(std::move(dataManager)), __func__); } RefPtr quotaManager = quota::QuotaManager::Get(); MOZ_ASSERT(quotaManager); QM_TRY_UNWRAP(auto streamTransportService, MOZ_TO_RESULT_GET_TYPED(nsCOMPtr, MOZ_SELECT_OVERLOAD(do_GetService), NS_STREAMTRANSPORTSERVICE_CONTRACTID), CreatePromise::CreateAndReject(NS_ERROR_FAILURE, __func__)); nsCString taskQueueName("OPFS "_ns + aOriginMetadata.mOrigin); RefPtr ioTaskQueue = TaskQueue::Create(do_AddRef(streamTransportService), taskQueueName.get()); auto dataManager = MakeRefPtr( aOriginMetadata, std::move(quotaManager), WrapMovingNotNull(streamTransportService), WrapMovingNotNull(ioTaskQueue)); AddFileSystemDataManager(aOriginMetadata.mOrigin, dataManager); return dataManager->BeginOpen()->Then( GetCurrentSerialEventTarget(), __func__, [dataManager = Registered(dataManager)]( const BoolPromise::ResolveOrRejectValue& aValue) { if (aValue.IsReject()) { return CreatePromise::CreateAndReject(aValue.RejectValue(), __func__); } return CreatePromise::CreateAndResolve(dataManager, __func__); }); } // static void FileSystemDataManager::AbortOperationsForLocks( const quota::Client::DirectoryLockIdTable& aDirectoryLockIds) { ::mozilla::ipc::AssertIsOnBackgroundThread(); // XXX Share the iteration code with `InitiateShutdown`, for example by // creating a helper function which would take a predicate function. if (!gDataManagers) { return; } for (const auto& dataManager : gDataManagers->Values()) { // Check if the Manager holds an acquired DirectoryLock. Origin clearing // can't be blocked by this Manager if there is no acquired DirectoryLock. // If there is an acquired DirectoryLock, check if the table contains the // lock for the Manager. if (quota::Client::IsLockForObjectAcquiredAndContainedInLockTable( *dataManager, aDirectoryLockIds)) { dataManager->RequestAllowToClose(); } } } // static void FileSystemDataManager::InitiateShutdown() { ::mozilla::ipc::AssertIsOnBackgroundThread(); if (!gDataManagers) { return; } for (const auto& dataManager : gDataManagers->Values()) { dataManager->RequestAllowToClose(); } } // static bool FileSystemDataManager::IsShutdownCompleted() { ::mozilla::ipc::AssertIsOnBackgroundThread(); return !gDataManagers; } void FileSystemDataManager::AssertIsOnIOTarget() const { DebugOnly current = false; MOZ_ASSERT(NS_SUCCEEDED(mIOTarget->IsOnCurrentThread(¤t))); MOZ_ASSERT(current); } void FileSystemDataManager::Register() { mRegCount++; } void FileSystemDataManager::Unregister() { MOZ_ASSERT(mRegCount > 0); mRegCount--; if (IsInactive()) { BeginClose(); } } void FileSystemDataManager::RegisterActor( NotNull aActor) { MOZ_ASSERT(!mBackgroundThreadAccessible.Access()->mActors.Contains(aActor)); mBackgroundThreadAccessible.Access()->mActors.Insert(aActor); #ifdef DEBUG aActor->SetRegistered(true); #endif } void FileSystemDataManager::UnregisterActor( NotNull aActor) { MOZ_ASSERT(mBackgroundThreadAccessible.Access()->mActors.Contains(aActor)); mBackgroundThreadAccessible.Access()->mActors.Remove(aActor); #ifdef DEBUG aActor->SetRegistered(false); #endif if (IsInactive()) { BeginClose(); } } void FileSystemDataManager::RegisterAccessHandle( NotNull aAccessHandle) { MOZ_ASSERT(!mBackgroundThreadAccessible.Access()->mAccessHandles.Contains( aAccessHandle)); mBackgroundThreadAccessible.Access()->mAccessHandles.Insert(aAccessHandle); } void FileSystemDataManager::UnregisterAccessHandle( NotNull aAccessHandle) { MOZ_ASSERT(mBackgroundThreadAccessible.Access()->mAccessHandles.Contains( aAccessHandle)); mBackgroundThreadAccessible.Access()->mAccessHandles.Remove(aAccessHandle); if (IsInactive()) { BeginClose(); } } RefPtr FileSystemDataManager::OnOpen() { MOZ_ASSERT(mState == State::Opening); return mOpenPromiseHolder.Ensure(__func__); } RefPtr FileSystemDataManager::OnClose() { MOZ_ASSERT(mState == State::Closing); return mClosePromiseHolder.Ensure(__func__); } // Note: Input can be temporary or main file id Result FileSystemDataManager::IsLocked( const FileId& aFileId) const { auto checkIfEntryIdIsLocked = [this, &aFileId]() -> Result { QM_TRY_INSPECT(const EntryId& entryId, mDatabaseManager->GetEntryId(aFileId)); return IsLocked(entryId); }; auto valueToSome = [](auto aValue) { return Some(std::move(aValue)); }; QM_TRY_UNWRAP(Maybe maybeLocked, QM_OR_ELSE_LOG_VERBOSE_IF( // Expression. (checkIfEntryIdIsLocked().map(valueToSome)), // Predicate. IsSpecificError, // Fallback. ([](const auto&) -> Result, QMResult> { return Some(false); // Non-existent files are not locked. }))); if (!maybeLocked) { // If the metadata is inaccessible, we block modifications. return true; } return *maybeLocked; } Result FileSystemDataManager::IsLocked( const EntryId& aEntryId) const { return mExclusiveLocks.Contains(aEntryId) || mSharedLocks.Contains(aEntryId); } Result FileSystemDataManager::LockExclusive( const EntryId& aEntryId) { QM_TRY_UNWRAP(const bool isLocked, IsLocked(aEntryId)); if (isLocked) { return Err(QMResult(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR)); } QM_TRY_INSPECT(const FileId& fileId, mDatabaseManager->EnsureFileId(aEntryId)); // If the file has been removed, we should get a file not found error. // Otherwise, if usage tracking cannot be started because file size is not // known and attempts to read it are failing, lock is denied to freeze the // quota usage until the (external) blocker is gone or the file is removed. QM_TRY(QM_TO_RESULT(mDatabaseManager->BeginUsageTracking(fileId))); LOG_VERBOSE(("ExclusiveLock")); mExclusiveLocks.Insert(aEntryId); return fileId; } // TODO: Improve reporting of failures, see bug 1840811. void FileSystemDataManager::UnlockExclusive(const EntryId& aEntryId) { MOZ_ASSERT(mExclusiveLocks.Contains(aEntryId)); LOG_VERBOSE(("ExclusiveUnlock")); mExclusiveLocks.Remove(aEntryId); QM_TRY_INSPECT(const FileId& fileId, mDatabaseManager->GetFileId(aEntryId), QM_VOID); // On error, usage tracking remains on to prevent writes until usage is // updated successfully. QM_TRY(MOZ_TO_RESULT(mDatabaseManager->UpdateUsage(fileId)), QM_VOID); QM_TRY(MOZ_TO_RESULT(mDatabaseManager->EndUsageTracking(fileId)), QM_VOID); } Result FileSystemDataManager::LockShared( const EntryId& aEntryId) { if (mExclusiveLocks.Contains(aEntryId)) { return Err(QMResult(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR)); } auto& count = mSharedLocks.LookupOrInsert(aEntryId); if (!(1u + CheckedUint32(count)).isValid()) { // don't make the count invalid return Err(QMResult(NS_ERROR_UNEXPECTED)); } QM_TRY_INSPECT(const FileId& fileId, mDatabaseManager->EnsureTemporaryFileId(aEntryId)); // If the file has been removed, we should get a file not found error. // Otherwise, if usage tracking cannot be started because file size is not // known and attempts to read it are failing, lock is denied to freeze the // quota usage until the (external) blocker is gone or the file is removed. QM_TRY(QM_TO_RESULT(mDatabaseManager->BeginUsageTracking(fileId))); ++count; LOG_VERBOSE(("SharedLock %u", count)); return fileId; } // TODO: Improve reporting of failures, see bug 1840811. void FileSystemDataManager::UnlockShared(const EntryId& aEntryId, const FileId& aFileId, bool aAbort) { MOZ_ASSERT(!mExclusiveLocks.Contains(aEntryId)); MOZ_ASSERT(mSharedLocks.Contains(aEntryId)); auto entry = mSharedLocks.Lookup(aEntryId); MOZ_ASSERT(entry); MOZ_ASSERT(entry.Data() > 0); --entry.Data(); LOG_VERBOSE(("SharedUnlock %u", *entry)); if (0u == entry.Data()) { entry.Remove(); } // On error, usage tracking remains on to prevent writes until usage is // updated successfully. QM_TRY(MOZ_TO_RESULT(mDatabaseManager->UpdateUsage(aFileId)), QM_VOID); QM_TRY(MOZ_TO_RESULT(mDatabaseManager->EndUsageTracking(aFileId)), QM_VOID); QM_TRY( MOZ_TO_RESULT(mDatabaseManager->MergeFileId(aEntryId, aFileId, aAbort)), QM_VOID); } FileMode FileSystemDataManager::GetMode(bool aKeepData) const { if (1 == mVersion) { return FileMode::EXCLUSIVE; } return aKeepData ? FileMode::SHARED_FROM_COPY : FileMode::SHARED_FROM_EMPTY; } bool FileSystemDataManager::IsInactive() const { auto data = mBackgroundThreadAccessible.Access(); return !mRegCount && !data->mActors.Count() && !data->mAccessHandles.Count(); } void FileSystemDataManager::RequestAllowToClose() { for (const auto& actor : mBackgroundThreadAccessible.Access()->mActors) { actor->RequestAllowToClose(); } } RefPtr FileSystemDataManager::BeginOpen() { MOZ_ASSERT(mQuotaManager); MOZ_ASSERT(mState == State::Initial); mState = State::Opening; mQuotaManager ->OpenClientDirectory( {mOriginMetadata, mozilla::dom::quota::Client::FILESYSTEM}) ->Then( GetCurrentSerialEventTarget(), __func__, [self = RefPtr(this)]( quota::ClientDirectoryLockPromise::ResolveOrRejectValue&& value) { if (value.IsReject()) { return BoolPromise::CreateAndReject(value.RejectValue(), __func__); } self->mDirectoryLock = std::move(value.ResolveValue()); return BoolPromise::CreateAndResolve(true, __func__); }) ->Then(mQuotaManager->IOThread(), __func__, [self = RefPtr(this)]( const BoolPromise::ResolveOrRejectValue& value) { if (value.IsReject()) { return BoolPromise::CreateAndReject(value.RejectValue(), __func__); } QM_TRY(MOZ_TO_RESULT( EnsureFileSystemDirectory(self->mOriginMetadata)), CreateAndRejectBoolPromise); return BoolPromise::CreateAndResolve(true, __func__); }) ->Then( MutableIOTaskQueuePtr(), __func__, [self = RefPtr(this)]( const BoolPromise::ResolveOrRejectValue& value) { if (value.IsReject()) { return BoolPromise::CreateAndReject(value.RejectValue(), __func__); } QM_TRY_UNWRAP(auto connection, GetStorageConnection(self->mOriginMetadata, self->mDirectoryLock->Id()), CreateAndRejectBoolPromiseFromQMResult); QM_TRY_UNWRAP(UniquePtr fmPtr, FileSystemFileManager::CreateFileSystemFileManager( self->mOriginMetadata), CreateAndRejectBoolPromiseFromQMResult); QM_TRY_UNWRAP( self->mVersion, QM_OR_ELSE_WARN_IF( // Expression. SchemaVersion002::InitializeConnection( connection, *fmPtr, self->mOriginMetadata.mOrigin), // Predicate. ([](const auto&) { return true; }), // Fallback. ([&self, &connection](const auto&) { QM_TRY_RETURN(SchemaVersion001::InitializeConnection( connection, self->mOriginMetadata.mOrigin)); })), CreateAndRejectBoolPromiseFromQMResult); QM_TRY_UNWRAP( EntryId rootId, fs::data::GetRootHandle(self->mOriginMetadata.mOrigin), CreateAndRejectBoolPromiseFromQMResult); switch (self->mVersion) { case 1: { self->mDatabaseManager = MakeUnique( self, std::move(connection), std::move(fmPtr), rootId); break; } case 2: { self->mDatabaseManager = MakeUnique( self, std::move(connection), std::move(fmPtr), rootId); break; } default: break; } return BoolPromise::CreateAndResolve(true, __func__); }) ->Then(GetCurrentSerialEventTarget(), __func__, [self = RefPtr(this)]( const BoolPromise::ResolveOrRejectValue& value) { if (value.IsReject()) { self->mState = State::Initial; self->mOpenPromiseHolder.RejectIfExists(value.RejectValue(), __func__); } else { self->mState = State::Open; self->mOpenPromiseHolder.ResolveIfExists(true, __func__); } }); return OnOpen(); } RefPtr FileSystemDataManager::BeginClose() { MOZ_ASSERT(mState != State::Closing && mState != State::Closed); MOZ_ASSERT(IsInactive()); mState = State::Closing; InvokeAsync(MutableIOTaskQueuePtr(), __func__, [self = RefPtr(this)]() { if (self->mDatabaseManager) { self->mDatabaseManager->Close(); self->mDatabaseManager = nullptr; } return BoolPromise::CreateAndResolve(true, __func__); }) ->Then(MutableBackgroundTargetPtr(), __func__, [self = RefPtr(this)]( const BoolPromise::ResolveOrRejectValue&) { return self->mIOTaskQueue->BeginShutdown(); }) ->Then(MutableBackgroundTargetPtr(), __func__, [self = RefPtr(this)]( const ShutdownPromise::ResolveOrRejectValue&) { self->mDirectoryLock = nullptr; RemoveFileSystemDataManager(self->mOriginMetadata.mOrigin); self->mState = State::Closed; self->mClosePromiseHolder.ResolveIfExists(true, __func__); }); return OnClose(); } } // namespace mozilla::dom::fs::data