/* -*- 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" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "ActorsParentCommon.h" #include "CrashAnnotations.h" #include "DatabaseFileInfo.h" #include "DatabaseFileManager.h" #include "DatabaseFileManagerImpl.h" #include "DBSchema.h" #include "ErrorList.h" #include "IDBCursorType.h" #include "IDBObjectStore.h" #include "IDBTransaction.h" #include "IndexedDBCommon.h" #include "IndexedDatabaseInlines.h" #include "IndexedDatabaseManager.h" #include "IndexedDBCipherKeyManager.h" #include "KeyPath.h" #include "MainThreadUtils.h" #include "ProfilerHelpers.h" #include "ReportInternalError.h" #include "SafeRefPtr.h" #include "SchemaUpgrades.h" #include "chrome/common/ipc_channel.h" #include "ipc/IPCMessageUtils.h" #include "js/RootingAPI.h" #include "js/StructuredClone.h" #include "js/Value.h" #include "jsapi.h" #include "mozIStorageAsyncConnection.h" #include "mozIStorageConnection.h" #include "mozIStorageFunction.h" #include "mozIStorageProgressHandler.h" #include "mozIStorageService.h" #include "mozIStorageStatement.h" #include "mozIStorageValueArray.h" #include "mozStorageCID.h" #include "mozStorageHelper.h" #include "mozilla/Algorithm.h" #include "mozilla/ArrayAlgorithm.h" #include "mozilla/ArrayIterator.h" #include "mozilla/Assertions.h" #include "mozilla/Atomics.h" #include "mozilla/Attributes.h" #include "mozilla/Casting.h" #include "mozilla/CondVar.h" #include "mozilla/DebugOnly.h" #include "mozilla/EndianUtils.h" #include "mozilla/ErrorNames.h" #include "mozilla/ErrorResult.h" #include "mozilla/InitializedOnce.h" #include "mozilla/Logging.h" #include "mozilla/MacroForEach.h" #include "mozilla/Maybe.h" #include "mozilla/Monitor.h" #include "mozilla/Mutex.h" #include "mozilla/NotNull.h" #include "mozilla/Preferences.h" #include "mozilla/ProfilerLabels.h" #include "mozilla/RefCountType.h" #include "mozilla/RefCounted.h" #include "mozilla/RemoteLazyInputStreamParent.h" #include "mozilla/RemoteLazyInputStreamStorage.h" #include "mozilla/Result.h" #include "mozilla/ResultExtensions.h" #include "mozilla/SchedulerGroup.h" #include "mozilla/SnappyCompressOutputStream.h" #include "mozilla/SpinEventLoopUntil.h" #include "mozilla/StaticPtr.h" #include "mozilla/TimeStamp.h" #include "mozilla/UniquePtr.h" #include "mozilla/Unused.h" #include "mozilla/Variant.h" #include "mozilla/dom/BlobImpl.h" #include "mozilla/dom/ContentParent.h" #include "mozilla/dom/FileBlobImpl.h" #include "mozilla/dom/FlippedOnce.h" #include "mozilla/dom/IDBCursorBinding.h" #include "mozilla/dom/IPCBlob.h" #include "mozilla/dom/IPCBlobUtils.h" #include "mozilla/dom/IndexedDatabase.h" #include "mozilla/dom/Nullable.h" #include "mozilla/dom/PContentParent.h" #include "mozilla/dom/ScriptSettings.h" #include "mozilla/dom/indexedDB/IDBResult.h" #include "mozilla/dom/indexedDB/Key.h" #include "mozilla/dom/indexedDB/PBackgroundIDBCursor.h" #include "mozilla/dom/indexedDB/PBackgroundIDBCursorParent.h" #include "mozilla/dom/indexedDB/PBackgroundIDBDatabase.h" #include "mozilla/dom/indexedDB/PBackgroundIDBDatabaseFileParent.h" #include "mozilla/dom/indexedDB/PBackgroundIDBDatabaseParent.h" #include "mozilla/dom/indexedDB/PBackgroundIDBFactory.h" #include "mozilla/dom/indexedDB/PBackgroundIDBFactoryParent.h" #include "mozilla/dom/indexedDB/PBackgroundIDBFactoryRequestParent.h" #include "mozilla/dom/indexedDB/PBackgroundIDBRequest.h" #include "mozilla/dom/indexedDB/PBackgroundIDBRequestParent.h" #include "mozilla/dom/indexedDB/PBackgroundIDBSharedTypes.h" #include "mozilla/dom/indexedDB/PBackgroundIDBTransactionParent.h" #include "mozilla/dom/indexedDB/PBackgroundIDBVersionChangeTransactionParent.h" #include "mozilla/dom/indexedDB/PBackgroundIndexedDBUtilsParent.h" #include "mozilla/dom/ipc/IdType.h" #include "mozilla/dom/quota/Assertions.h" #include "mozilla/dom/quota/CachingDatabaseConnection.h" #include "mozilla/dom/quota/CheckedUnsafePtr.h" #include "mozilla/dom/quota/Client.h" #include "mozilla/dom/quota/ClientImpl.h" #include "mozilla/dom/quota/DirectoryLock.h" #include "mozilla/dom/quota/DecryptingInputStream_impl.h" #include "mozilla/dom/quota/EncryptingOutputStream_impl.h" #include "mozilla/dom/quota/FileStreams.h" #include "mozilla/dom/quota/OriginScope.h" #include "mozilla/dom/quota/PersistenceType.h" #include "mozilla/dom/quota/QuotaCommon.h" #include "mozilla/dom/quota/QuotaManager.h" #include "mozilla/dom/quota/QuotaObject.h" #include "mozilla/dom/quota/ResultExtensions.h" #include "mozilla/dom/quota/UsageInfo.h" #include "mozilla/fallible.h" #include "mozilla/ipc/BackgroundParent.h" #include "mozilla/ipc/BackgroundUtils.h" #include "mozilla/ipc/InputStreamParams.h" #include "mozilla/ipc/PBackgroundParent.h" #include "mozilla/ipc/PBackgroundSharedTypes.h" #include "mozilla/ipc/ProtocolUtils.h" #include "mozilla/mozalloc.h" #include "mozilla/storage/Variant.h" #include "nsBaseHashtable.h" #include "nsCOMPtr.h" #include "nsClassHashtable.h" #include "nsContentUtils.h" #include "nsTHashMap.h" #include "nsDebug.h" #include "nsError.h" #include "nsEscape.h" #include "nsHashKeys.h" #include "nsIAsyncInputStream.h" #include "nsID.h" #include "nsIDUtils.h" #include "nsIDirectoryEnumerator.h" #include "nsIEventTarget.h" #include "nsIFile.h" #include "nsIFileProtocolHandler.h" #include "nsIFileStreams.h" #include "nsIFileURL.h" #include "nsIInputStream.h" #include "nsIOutputStream.h" #include "nsIProtocolHandler.h" #include "nsIRunnable.h" #include "nsISupports.h" #include "nsISupportsPriority.h" #include "nsISupportsUtils.h" #include "nsIThread.h" #include "nsIThreadInternal.h" #include "nsITimer.h" #include "nsIURIMutator.h" #include "nsIVariant.h" #include "nsLiteralString.h" #include "nsNetCID.h" #include "nsPrintfCString.h" #include "nsProxyRelease.h" #include "nsServiceManagerUtils.h" #include "nsStreamUtils.h" #include "nsString.h" #include "nsStringFlags.h" #include "nsStringFwd.h" #include "nsTArray.h" #include "nsTHashSet.h" #include "nsTHashtable.h" #include "nsTLiteralString.h" #include "nsTStringRepr.h" #include "nsThreadPool.h" #include "nsThreadUtils.h" #include "nscore.h" #include "prinrval.h" #include "prio.h" #include "prsystem.h" #include "prthread.h" #include "prtime.h" #include "prtypes.h" #include "snappy/snappy.h" struct JSContext; class JSObject; template class nsPtrHashKey; #define IDB_DEBUG_LOG(_args) \ MOZ_LOG(IndexedDatabaseManager::GetLoggingModule(), LogLevel::Debug, _args) #if defined(MOZ_WIDGET_ANDROID) # define IDB_MOBILE #endif // Helper macros to reduce assertion verbosity // AUUF == ASSERT_UNREACHABLE_UNLESS_FUZZING #ifdef DEBUG # ifdef FUZZING # define NS_AUUF_OR_WARN(...) NS_WARNING(__VA_ARGS__) # else # define NS_AUUF_OR_WARN(...) MOZ_ASSERT(false, __VA_ARGS__) # endif # define NS_AUUF_OR_WARN_IF(cond) \ [](bool aCond) { \ if (MOZ_UNLIKELY(aCond)) { \ NS_AUUF_OR_WARN(#cond); \ } \ return aCond; \ }((cond)) #else # define NS_AUUF_OR_WARN(...) \ do { \ } while (false) # define NS_AUUF_OR_WARN_IF(cond) static_cast(cond) #endif namespace mozilla { namespace dom::indexedDB { using namespace mozilla::dom::quota; using namespace mozilla::ipc; using mozilla::dom::quota::Client; namespace { class ConnectionPool; class Database; struct DatabaseActorInfo; class DatabaseFile; class DatabaseLoggingInfo; class DatabaseMaintenance; class Factory; class Maintenance; class OpenDatabaseOp; class TransactionBase; class TransactionDatabaseOperationBase; class VersionChangeTransaction; template struct ValuePopulateResponseHelper; /******************************************************************************* * Constants ******************************************************************************/ const int32_t kStorageProgressGranularity = 1000; // Changing the value here will override the page size of new databases only. // A journal mode change and VACUUM are needed to change existing databases, so // the best way to do that is to use the schema version upgrade mechanism. const uint32_t kSQLitePageSizeOverride = #ifdef IDB_MOBILE 2048; #else 4096; #endif static_assert(kSQLitePageSizeOverride == /* mozStorage default */ 0 || (kSQLitePageSizeOverride % 2 == 0 && kSQLitePageSizeOverride >= 512 && kSQLitePageSizeOverride <= 65536), "Must be 0 (disabled) or a power of 2 between 512 and 65536!"); // Set to -1 to use SQLite's default, 0 to disable, or some positive number to // enforce a custom limit. const int32_t kMaxWALPages = 5000; // 20MB on desktop, 10MB on mobile. // Set to some multiple of the page size to grow the database in larger chunks. const uint32_t kSQLiteGrowthIncrement = kSQLitePageSizeOverride * 2; static_assert(kSQLiteGrowthIncrement >= 0 && kSQLiteGrowthIncrement % kSQLitePageSizeOverride == 0 && kSQLiteGrowthIncrement < uint32_t(INT32_MAX), "Must be 0 (disabled) or a positive multiple of the page size!"); // The maximum number of threads that can be used for database activity at a // single time. const uint32_t kMaxConnectionThreadCount = 20; static_assert(kMaxConnectionThreadCount, "Must have at least one thread!"); // The maximum number of threads to keep when idle. Threads that become idle in // excess of this number will be shut down immediately. const uint32_t kMaxIdleConnectionThreadCount = 2; static_assert(kMaxConnectionThreadCount >= kMaxIdleConnectionThreadCount, "Idle thread limit must be less than total thread limit!"); // The length of time that database connections will be held open after all // transactions have completed before doing idle maintenance. const uint32_t kConnectionIdleMaintenanceMS = 2 * 1000; // 2 seconds // The length of time that database connections will be held open after all // transactions and maintenance have completed. const uint32_t kConnectionIdleCloseMS = 10 * 1000; // 10 seconds // The length of time that idle threads will stay alive before being shut down. const uint32_t kConnectionThreadIdleMS = 30 * 1000; // 30 seconds #define SAVEPOINT_CLAUSE "SAVEPOINT sp;"_ns // For efficiency reasons, kEncryptedStreamBlockSize must be a multiple of large // 4k disk sectors. static_assert(kEncryptedStreamBlockSize % 4096 == 0); // Similarly, the file copy buffer size must be a multiple of the encrypted // block size. static_assert(kFileCopyBufferSize % kEncryptedStreamBlockSize == 0); constexpr auto kFileManagerDirectoryNameSuffix = u".files"_ns; constexpr auto kSQLiteSuffix = u".sqlite"_ns; constexpr auto kSQLiteJournalSuffix = u".sqlite-journal"_ns; constexpr auto kSQLiteSHMSuffix = u".sqlite-shm"_ns; constexpr auto kSQLiteWALSuffix = u".sqlite-wal"_ns; // The following constants define all names of binding parameters in statements, // where they are bound by name. This should include all parameter names which // are bound by name. Binding may be done by index when the statement definition // and binding are done in the same local scope, and no other reasons prevent // using the indexes (e.g. multiple statement variants with differing number or // order of parameters). Neither the styles of specifying parameter names // (literally vs. via these constants) nor the binding styles (by index vs. by // name) should not be mixed for the same statement. The decision must be made // for each statement based on the proximity of statement and binding calls. constexpr auto kStmtParamNameCurrentKey = "current_key"_ns; constexpr auto kStmtParamNameRangeBound = "range_bound"_ns; constexpr auto kStmtParamNameObjectStorePosition = "object_store_position"_ns; constexpr auto kStmtParamNameLowerKey = "lower_key"_ns; constexpr auto kStmtParamNameUpperKey = "upper_key"_ns; constexpr auto kStmtParamNameKey = "key"_ns; constexpr auto kStmtParamNameObjectStoreId = "object_store_id"_ns; constexpr auto kStmtParamNameIndexId = "index_id"_ns; // TODO: Maybe the uses of kStmtParamNameId should be replaced by more // specific constants such as kStmtParamNameObjectStoreId. constexpr auto kStmtParamNameId = "id"_ns; constexpr auto kStmtParamNameValue = "value"_ns; constexpr auto kStmtParamNameObjectDataKey = "object_data_key"_ns; constexpr auto kStmtParamNameIndexDataValues = "index_data_values"_ns; constexpr auto kStmtParamNameData = "data"_ns; constexpr auto kStmtParamNameFileIds = "file_ids"_ns; constexpr auto kStmtParamNameValueLocale = "value_locale"_ns; constexpr auto kStmtParamNameLimit = "limit"_ns; // The following constants define some names of columns in tables, which are // referred to in remote locations, e.g. in calls to // GetBindingClauseForKeyRange. constexpr auto kColumnNameKey = "key"_ns; constexpr auto kColumnNameValue = "value"_ns; constexpr auto kColumnNameAliasSortKey = "sort_column"_ns; // SQL fragments used at multiple locations. constexpr auto kOpenLimit = " LIMIT "_ns; // The deletion marker file is created before RemoveDatabaseFilesAndDirectory // begins deleting a database. It is removed as the last step of deletion. If a // deletion marker file is found when initializing the origin, the deletion // routine is run again to ensure that the database and all of its related files // are removed. The primary goal of this mechanism is to avoid situations where // a database has been partially deleted, leading to inconsistent state for the // origin. constexpr auto kIdbDeletionMarkerFilePrefix = u"idb-deleting-"_ns; const uint32_t kDeleteTimeoutMs = 1000; #ifdef DEBUG const int32_t kDEBUGThreadPriority = nsISupportsPriority::PRIORITY_NORMAL; const uint32_t kDEBUGThreadSleepMS = 0; const int32_t kDEBUGTransactionThreadPriority = nsISupportsPriority::PRIORITY_NORMAL; const uint32_t kDEBUGTransactionThreadSleepMS = 0; #endif /******************************************************************************* * Metadata classes ******************************************************************************/ // Can be instantiated either on the QuotaManager IO thread or on a // versionchange transaction thread. These threads can never race so this is // totally safe. struct FullIndexMetadata { IndexMetadata mCommonMetadata = {0, nsString(), KeyPath(0), nsCString(), false, false, false}; FlippedOnce mDeleted; NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FullIndexMetadata) private: ~FullIndexMetadata() = default; }; using IndexTable = nsTHashMap>; // Can be instantiated either on the QuotaManager IO thread or on a // versionchange transaction thread. These threads can never race so this is // totally safe. struct FullObjectStoreMetadata { ObjectStoreMetadata mCommonMetadata; IndexTable mIndexes; // The auto increment ids are touched on both the background thread and the // transaction I/O thread, and they must be kept in sync, so we need a mutex // to protect them. struct AutoIncrementIds { int64_t next; int64_t committed; }; DataMutex mAutoIncrementIds; FlippedOnce mDeleted; NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FullObjectStoreMetadata); bool HasLiveIndexes() const; FullObjectStoreMetadata(ObjectStoreMetadata aCommonMetadata, const AutoIncrementIds& aAutoIncrementIds) : mCommonMetadata{std::move(aCommonMetadata)}, mAutoIncrementIds{AutoIncrementIds{aAutoIncrementIds}, "FullObjectStoreMetadata"} {} private: ~FullObjectStoreMetadata() = default; }; using ObjectStoreTable = nsTHashMap>; static_assert( std::is_same_v() .objectStoreId())>>>); static_assert( std::is_same_v< IndexOrObjectStoreId, std::remove_cv_t().objectStoreId())>>>); struct FullDatabaseMetadata final : AtomicSafeRefCounted { DatabaseMetadata mCommonMetadata; nsCString mDatabaseId; nsString mFilePath; ObjectStoreTable mObjectStores; IndexOrObjectStoreId mNextObjectStoreId = 0; IndexOrObjectStoreId mNextIndexId = 0; public: explicit FullDatabaseMetadata(const DatabaseMetadata& aCommonMetadata) : mCommonMetadata(aCommonMetadata) { AssertIsOnBackgroundThread(); } [[nodiscard]] SafeRefPtr Duplicate() const; MOZ_DECLARE_REFCOUNTED_TYPENAME(FullDatabaseMetadata) }; template auto MatchMetadataNameOrId(const Enumerable& aEnumerable, IndexOrObjectStoreId aId, Maybe aName = Nothing()) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aId); const auto it = std::find_if( aEnumerable.cbegin(), aEnumerable.cend(), [aId, aName](const auto& entry) { MOZ_ASSERT(entry.GetKey() != 0); const auto& value = entry.GetData(); MOZ_ASSERT(value); return !value->mDeleted && (aId == value->mCommonMetadata.id() || (aName && *aName == value->mCommonMetadata.name())); }); return ToMaybeRef(it != aEnumerable.cend() ? it->GetData().unsafeGetRawPtr() : nullptr); } /******************************************************************************* * SQLite functions ******************************************************************************/ // WARNING: the hash function used for the database name must not change. // That's why this function exists separately from mozilla::HashString(), even // though it is (at the time of writing) equivalent. See bug 780408 and bug // 940315 for details. uint32_t HashName(const nsAString& aName) { struct Helper { static uint32_t RotateBitsLeft32(uint32_t aValue, uint8_t aBits) { MOZ_ASSERT(aBits < 32); return (aValue << aBits) | (aValue >> (32 - aBits)); } }; static const uint32_t kGoldenRatioU32 = 0x9e3779b9u; return std::accumulate(aName.BeginReading(), aName.EndReading(), uint32_t(0), [](uint32_t hash, char16_t ch) { return kGoldenRatioU32 * (Helper::RotateBitsLeft32(hash, 5) ^ ch); }); } nsresult ClampResultCode(nsresult aResultCode) { if (NS_SUCCEEDED(aResultCode) || NS_ERROR_GET_MODULE(aResultCode) == NS_ERROR_MODULE_DOM_INDEXEDDB) { return aResultCode; } switch (aResultCode) { case NS_ERROR_FILE_NO_DEVICE_SPACE: return NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR; case NS_ERROR_STORAGE_CONSTRAINT: return NS_ERROR_DOM_INDEXEDDB_CONSTRAINT_ERR; default: #ifdef DEBUG nsPrintfCString message("Converting non-IndexedDB error code (0x%" PRIX32 ") to " "NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR", static_cast(aResultCode)); NS_WARNING(message.get()); #else ; #endif } IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } Result, nsresult> GetDatabaseFileURL( nsIFile& aDatabaseFile, const int64_t aDirectoryLockId, const Maybe& aMaybeKey) { MOZ_ASSERT(aDirectoryLockId >= -1); QM_TRY_INSPECT( const auto& protocolHandler, MOZ_TO_RESULT_GET_TYPED(nsCOMPtr, MOZ_SELECT_OVERLOAD(do_GetService), NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX "file")); QM_TRY_INSPECT(const auto& fileHandler, MOZ_TO_RESULT_GET_TYPED(nsCOMPtr, MOZ_SELECT_OVERLOAD(do_QueryInterface), protocolHandler)); QM_TRY_INSPECT(const auto& mutator, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( nsCOMPtr, fileHandler, NewFileURIMutator, &aDatabaseFile)); // aDirectoryLockId should only be -1 when we are called // - from DatabaseFileManager::InitDirectory when the temporary storage // hasn't been initialized yet. At that time, the in-memory objects (e.g. // OriginInfo) are only being created so it doesn't make sense to tunnel // quota information to QuotaVFS to get corresponding QuotaObject instances // for SQLite files. // - from DeleteDatabaseOp::LoadPreviousVersion, since this might require // temporarily exceeding the quota limit before the database can be // deleted. const nsCString directoryLockIdClause = "&directoryLockId="_ns + IntToCString(aDirectoryLockId); const auto keyClause = [&aMaybeKey] { nsAutoCString keyClause; if (aMaybeKey) { keyClause.AssignLiteral("&key="); for (uint8_t byte : IndexedDBCipherStrategy::SerializeKey(*aMaybeKey)) { keyClause.AppendPrintf("%02x", byte); } } return keyClause; }(); QM_TRY_UNWRAP(auto result, ([&mutator, &directoryLockIdClause, &keyClause] { nsCOMPtr result; nsresult rv = NS_MutateURI(mutator) .SetQuery("cache=private"_ns + directoryLockIdClause + keyClause) .Finalize(result); return NS_SUCCEEDED(rv) ? Result, nsresult>{result} : Err(rv); }())); return result; } nsresult SetDefaultPragmas(mozIStorageConnection& aConnection) { MOZ_ASSERT(!NS_IsMainThread()); static constexpr auto kBuiltInPragmas = // We use foreign keys in DEBUG builds only because there is a performance // cost to using them. "PRAGMA foreign_keys = " #ifdef DEBUG "ON" #else "OFF" #endif ";" // The "INSERT OR REPLACE" statement doesn't fire the update trigger, // instead it fires only the insert trigger. This confuses the update // refcount function. This behavior changes with enabled recursive // triggers, so the statement fires the delete trigger first and then the // insert trigger. "PRAGMA recursive_triggers = ON;" // We aggressively truncate the database file when idle so don't bother // overwriting the WAL with 0 during active periods. "PRAGMA secure_delete = OFF;"_ns; QM_TRY(MOZ_TO_RESULT(aConnection.ExecuteSimpleSQL(kBuiltInPragmas))); QM_TRY(MOZ_TO_RESULT(aConnection.ExecuteSimpleSQL(nsAutoCString{ "PRAGMA synchronous = "_ns + (IndexedDatabaseManager::FullSynchronous() ? "FULL"_ns : "NORMAL"_ns) + ";"_ns}))); #ifndef IDB_MOBILE if (kSQLiteGrowthIncrement) { // This is just an optimization so ignore the failure if the disk is // currently too full. QM_TRY(QM_OR_ELSE_WARN_IF( // Expression. MOZ_TO_RESULT( aConnection.SetGrowthIncrement(kSQLiteGrowthIncrement, ""_ns)), // Predicate. IsSpecificError, // Fallback. ErrToDefaultOk<>)); } #endif // IDB_MOBILE return NS_OK; } nsresult SetJournalMode(mozIStorageConnection& aConnection) { MOZ_ASSERT(!NS_IsMainThread()); // Try enabling WAL mode. This can fail in various circumstances so we have to // check the results here. constexpr auto journalModeQueryStart = "PRAGMA journal_mode = "_ns; constexpr auto journalModeWAL = "wal"_ns; QM_TRY_INSPECT(const auto& stmt, CreateAndExecuteSingleStepStatement( aConnection, journalModeQueryStart + journalModeWAL)); QM_TRY_INSPECT( const auto& journalMode, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, *stmt, GetUTF8String, 0)); if (journalMode.Equals(journalModeWAL)) { // WAL mode successfully enabled. Maybe set limits on its size here. if (kMaxWALPages >= 0) { QM_TRY(MOZ_TO_RESULT(aConnection.ExecuteSimpleSQL( "PRAGMA wal_autocheckpoint = "_ns + IntToCString(kMaxWALPages)))); } } else { NS_WARNING("Failed to set WAL mode, falling back to normal journal mode."); #ifdef IDB_MOBILE QM_TRY(MOZ_TO_RESULT( aConnection.ExecuteSimpleSQL(journalModeQueryStart + "truncate"_ns))); #endif } return NS_OK; } Result>, nsresult> OpenDatabase( mozIStorageService& aStorageService, nsIFileURL& aFileURL, const uint32_t aTelemetryId = 0) { const nsAutoCString telemetryFilename = aTelemetryId ? "indexedDB-"_ns + IntToCString(aTelemetryId) + NS_ConvertUTF16toUTF8(kSQLiteSuffix) : nsAutoCString(); QM_TRY_UNWRAP(auto connection, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( nsCOMPtr, aStorageService, OpenDatabaseWithFileURL, &aFileURL, telemetryFilename, mozIStorageService::CONNECTION_INTERRUPTIBLE)); return WrapMovingNotNull(std::move(connection)); } Result>, nsresult> OpenDatabaseAndHandleBusy(mozIStorageService& aStorageService, nsIFileURL& aFileURL, const uint32_t aTelemetryId = 0) { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!IsOnBackgroundThread()); using ConnectionType = Maybe>>; QM_TRY_UNWRAP(auto connection, QM_OR_ELSE_WARN_IF( // Expression OpenDatabase(aStorageService, aFileURL, aTelemetryId) .map([](auto connection) -> ConnectionType { return Some(std::move(connection)); }), // Predicate. IsSpecificError, // Fallback. ErrToDefaultOk)); if (connection.isNothing()) { #ifdef DEBUG { nsCString path; MOZ_ALWAYS_SUCCEEDS(aFileURL.GetFileName(path)); nsPrintfCString message( "Received NS_ERROR_STORAGE_BUSY when attempting to open database " "'%s', retrying for up to 10 seconds", path.get()); NS_WARNING(message.get()); } #endif // Another thread must be checkpointing the WAL. Wait up to 10 seconds for // that to complete. const TimeStamp start = TimeStamp::NowLoRes(); do { PR_Sleep(PR_MillisecondsToInterval(100)); QM_TRY_UNWRAP(connection, QM_OR_ELSE_WARN_IF( // Expression. OpenDatabase(aStorageService, aFileURL, aTelemetryId) .map([](auto connection) -> ConnectionType { return Some(std::move(connection)); }), // Predicate. ([&start](nsresult aValue) { return aValue == NS_ERROR_STORAGE_BUSY && TimeStamp::NowLoRes() - start <= TimeDuration::FromSeconds(10); }), // Fallback. ErrToDefaultOk)); } while (connection.isNothing()); } return connection.extract(); } // Returns true if a given nsIFile exists and is a directory. Returns false if // it doesn't exist. Returns an error if it exists, but is not a directory, or // any other error occurs. Result ExistsAsDirectory(nsIFile& aDirectory) { QM_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE_MEMBER(aDirectory, Exists)); if (exists) { QM_TRY_INSPECT(const bool& isDirectory, MOZ_TO_RESULT_INVOKE_MEMBER(aDirectory, IsDirectory)); QM_TRY(OkIf(isDirectory), Err(NS_ERROR_FAILURE)); } return exists; } constexpr nsresult mapNoDeviceSpaceError(nsresult aRv) { if (aRv == NS_ERROR_FILE_NO_DEVICE_SPACE) { // mozstorage translates SQLITE_FULL to // NS_ERROR_FILE_NO_DEVICE_SPACE, which we know better as // NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR. return NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR; } return aRv; } Result>, nsresult> CreateStorageConnection(nsIFile& aDBFile, nsIFile& aFMDirectory, const nsAString& aName, const nsACString& aOrigin, const int64_t aDirectoryLockId, const uint32_t aTelemetryId, const Maybe& aMaybeKey) { AssertIsOnIOThread(); MOZ_ASSERT(aDirectoryLockId >= -1); AUTO_PROFILER_LABEL("CreateStorageConnection", DOM); QM_TRY_INSPECT(const auto& dbFileUrl, GetDatabaseFileURL(aDBFile, aDirectoryLockId, aMaybeKey)); QM_TRY_INSPECT(const auto& storageService, MOZ_TO_RESULT_GET_TYPED(nsCOMPtr, MOZ_SELECT_OVERLOAD(do_GetService), MOZ_STORAGE_SERVICE_CONTRACTID)); QM_TRY_UNWRAP( auto connection, QM_OR_ELSE_WARN_IF( // Expression. OpenDatabaseAndHandleBusy(*storageService, *dbFileUrl, aTelemetryId) .map([](auto connection) -> nsCOMPtr { return std::move(connection).unwrapBasePtr(); }), // Predicate. ([&aName](nsresult aValue) { // If we're just opening the database during origin initialization, // then we don't want to erase any files. The failure here will fail // origin initialization too. return IsDatabaseCorruptionError(aValue) && !aName.IsVoid(); }), // Fallback. ErrToDefaultOk>)); if (!connection) { // XXX Shouldn't we also update quota usage? // Nuke the database file. QM_TRY(MOZ_TO_RESULT(aDBFile.Remove(false))); QM_TRY_INSPECT(const bool& existsAsDirectory, ExistsAsDirectory(aFMDirectory)); if (existsAsDirectory) { QM_TRY(MOZ_TO_RESULT(aFMDirectory.Remove(true))); } QM_TRY_UNWRAP(connection, OpenDatabaseAndHandleBusy( *storageService, *dbFileUrl, aTelemetryId)); } QM_TRY(MOZ_TO_RESULT(SetDefaultPragmas(*connection))); QM_TRY(MOZ_TO_RESULT(connection->EnableModule("filesystem"_ns))); // Check to make sure that the database schema is correct. QM_TRY_INSPECT(const int32_t& schemaVersion, MOZ_TO_RESULT_INVOKE_MEMBER(connection, GetSchemaVersion)); // Unknown schema will fail origin initialization too. QM_TRY(OkIf(schemaVersion || !aName.IsVoid()), Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR), [](const auto&) { IDB_WARNING("Unable to open IndexedDB database, schema is not set!"); }); QM_TRY( OkIf(schemaVersion <= kSQLiteSchemaVersion), Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR), [](const auto&) { IDB_WARNING("Unable to open IndexedDB database, schema is too high!"); }); bool journalModeSet = false; if (schemaVersion != kSQLiteSchemaVersion) { const bool newDatabase = !schemaVersion; if (newDatabase) { // Set the page size first. const auto sqlitePageSizeOverride = aMaybeKey ? 8192 : kSQLitePageSizeOverride; if (sqlitePageSizeOverride) { QM_TRY(MOZ_TO_RESULT(connection->ExecuteSimpleSQL(nsPrintfCString( "PRAGMA page_size = %" PRIu32 ";", sqlitePageSizeOverride)))); } // We have to set the auto_vacuum mode before opening a transaction. QM_TRY((MOZ_TO_RESULT_INVOKE_MEMBER( connection, ExecuteSimpleSQL, #ifdef IDB_MOBILE // Turn on full auto_vacuum mode to reclaim disk space on // mobile devices (at the cost of some COMMIT speed). "PRAGMA auto_vacuum = FULL;"_ns #else // Turn on incremental auto_vacuum mode on desktop builds. "PRAGMA auto_vacuum = INCREMENTAL;"_ns #endif ) .mapErr(mapNoDeviceSpaceError))); QM_TRY(MOZ_TO_RESULT(SetJournalMode(*connection))); journalModeSet = true; } else { #ifdef DEBUG // Disable foreign key support while upgrading. This has to be done before // starting a transaction. MOZ_ALWAYS_SUCCEEDS( connection->ExecuteSimpleSQL("PRAGMA foreign_keys = OFF;"_ns)); #endif } bool vacuumNeeded = false; mozStorageTransaction transaction( connection.get(), false, mozIStorageConnection::TRANSACTION_IMMEDIATE); QM_TRY(MOZ_TO_RESULT(transaction.Start())); if (newDatabase) { QM_TRY(MOZ_TO_RESULT(CreateTables(*connection))); #ifdef DEBUG { QM_TRY_INSPECT( const int32_t& schemaVersion, MOZ_TO_RESULT_INVOKE_MEMBER(connection, GetSchemaVersion), QM_ASSERT_UNREACHABLE); MOZ_ASSERT(schemaVersion == kSQLiteSchemaVersion); } #endif // The parameter names are not used, parameters are bound by index only // locally in the same function. QM_TRY_INSPECT( const auto& stmt, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( nsCOMPtr, connection, CreateStatement, "INSERT INTO database (name, origin) " "VALUES (:name, :origin)"_ns)); QM_TRY(MOZ_TO_RESULT(stmt->BindStringByIndex(0, aName))); QM_TRY(MOZ_TO_RESULT(stmt->BindUTF8StringByIndex(1, aOrigin))); QM_TRY(MOZ_TO_RESULT(stmt->Execute())); } else { QM_TRY_UNWRAP(vacuumNeeded, MaybeUpgradeSchema(*connection, schemaVersion, aFMDirectory, aOrigin)); } QM_TRY(MOZ_TO_RESULT_INVOKE_MEMBER(transaction, Commit) .mapErr(mapNoDeviceSpaceError)); #ifdef DEBUG if (!newDatabase) { // Re-enable foreign key support after doing a foreign key check. QM_TRY_INSPECT(const bool& foreignKeyError, CreateAndExecuteSingleStepStatement< SingleStepResult::ReturnNullIfNoResult>( *connection, "PRAGMA foreign_key_check;"_ns), QM_ASSERT_UNREACHABLE); MOZ_ASSERT(!foreignKeyError, "Database has inconsisistent foreign keys!"); MOZ_ALWAYS_SUCCEEDS( connection->ExecuteSimpleSQL("PRAGMA foreign_keys = OFF;"_ns)); } #endif if (kSQLitePageSizeOverride && !newDatabase) { QM_TRY_INSPECT(const auto& stmt, CreateAndExecuteSingleStepStatement( *connection, "PRAGMA page_size;"_ns)); QM_TRY_INSPECT(const int32_t& pageSize, MOZ_TO_RESULT_INVOKE_MEMBER(*stmt, GetInt32, 0)); MOZ_ASSERT(pageSize >= 512 && pageSize <= 65536); if (kSQLitePageSizeOverride != uint32_t(pageSize)) { // We must not be in WAL journal mode to change the page size. QM_TRY(MOZ_TO_RESULT( connection->ExecuteSimpleSQL("PRAGMA journal_mode = DELETE;"_ns))); QM_TRY_INSPECT(const auto& stmt, CreateAndExecuteSingleStepStatement( *connection, "PRAGMA journal_mode;"_ns)); QM_TRY_INSPECT(const auto& journalMode, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, *stmt, GetUTF8String, 0)); if (journalMode.EqualsLiteral("delete")) { // Successfully set to rollback journal mode so changing the page size // is possible with a VACUUM. QM_TRY(MOZ_TO_RESULT(connection->ExecuteSimpleSQL(nsPrintfCString( "PRAGMA page_size = %" PRIu32 ";", kSQLitePageSizeOverride)))); // We will need to VACUUM in order to change the page size. vacuumNeeded = true; } else { NS_WARNING( "Failed to set journal_mode for database, unable to " "change the page size!"); } } } if (vacuumNeeded) { QM_TRY(MOZ_TO_RESULT(connection->ExecuteSimpleSQL("VACUUM;"_ns))); } if (newDatabase || vacuumNeeded) { if (journalModeSet) { // Make sure we checkpoint to get an accurate file size. QM_TRY(MOZ_TO_RESULT( connection->ExecuteSimpleSQL("PRAGMA wal_checkpoint(FULL);"_ns))); } QM_TRY_INSPECT(const int64_t& fileSize, MOZ_TO_RESULT_INVOKE_MEMBER(aDBFile, GetFileSize)); MOZ_ASSERT(fileSize > 0); PRTime vacuumTime = PR_Now(); MOZ_ASSERT(vacuumTime); // The parameter names are not used, parameters are bound by index only // locally in the same function. QM_TRY_INSPECT( const auto& vacuumTimeStmt, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCOMPtr, connection, CreateStatement, "UPDATE database " "SET last_vacuum_time = :time" ", last_vacuum_size = :size;"_ns)); QM_TRY(MOZ_TO_RESULT(vacuumTimeStmt->BindInt64ByIndex(0, vacuumTime))); QM_TRY(MOZ_TO_RESULT(vacuumTimeStmt->BindInt64ByIndex(1, fileSize))); QM_TRY(MOZ_TO_RESULT(vacuumTimeStmt->Execute())); } } if (!journalModeSet) { QM_TRY(MOZ_TO_RESULT(SetJournalMode(*connection))); } return WrapMovingNotNullUnchecked(std::move(connection)); } nsCOMPtr GetFileForPath(const nsAString& aPath) { MOZ_ASSERT(!aPath.IsEmpty()); QM_TRY_RETURN(QM_NewLocalFile(aPath), nullptr); } Result>, nsresult> GetStorageConnection(nsIFile& aDatabaseFile, const int64_t aDirectoryLockId, const uint32_t aTelemetryId, const Maybe& aMaybeKey) { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(aDirectoryLockId >= 0); AUTO_PROFILER_LABEL("GetStorageConnection", DOM); QM_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE_MEMBER(aDatabaseFile, Exists)); QM_TRY(OkIf(exists), Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR), IDB_REPORT_INTERNAL_ERR_LAMBDA); QM_TRY_INSPECT( const auto& dbFileUrl, GetDatabaseFileURL(aDatabaseFile, aDirectoryLockId, aMaybeKey)); QM_TRY_INSPECT(const auto& storageService, MOZ_TO_RESULT_GET_TYPED(nsCOMPtr, MOZ_SELECT_OVERLOAD(do_GetService), MOZ_STORAGE_SERVICE_CONTRACTID)); QM_TRY_UNWRAP( nsCOMPtr connection, OpenDatabaseAndHandleBusy(*storageService, *dbFileUrl, aTelemetryId)); QM_TRY(MOZ_TO_RESULT(SetDefaultPragmas(*connection))); QM_TRY(MOZ_TO_RESULT(SetJournalMode(*connection))); return WrapMovingNotNullUnchecked(std::move(connection)); } Result>, nsresult> GetStorageConnection(const nsAString& aDatabaseFilePath, const int64_t aDirectoryLockId, const uint32_t aTelemetryId, const Maybe& aMaybeKey) { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(!aDatabaseFilePath.IsEmpty()); MOZ_ASSERT(StringEndsWith(aDatabaseFilePath, kSQLiteSuffix)); MOZ_ASSERT(aDirectoryLockId >= 0); nsCOMPtr dbFile = GetFileForPath(aDatabaseFilePath); QM_TRY(OkIf(dbFile), Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR), IDB_REPORT_INTERNAL_ERR_LAMBDA); return GetStorageConnection(*dbFile, aDirectoryLockId, aTelemetryId, aMaybeKey); } /******************************************************************************* * ConnectionPool declarations ******************************************************************************/ class DatabaseConnection final : public CachingDatabaseConnection { friend class ConnectionPool; enum class CheckpointMode { Full, Restart, Truncate }; public: class AutoSavepoint; class UpdateRefcountFunction; private: InitializedOnce>> mFileManager; RefPtr mUpdateRefcountFunction; RefPtr mQuotaObject; RefPtr mJournalQuotaObject; bool mInReadTransaction; bool mInWriteTransaction; #ifdef DEBUG uint32_t mDEBUGSavepointCount; #endif public: NS_INLINE_DECL_THREADSAFE_REFCOUNTING(DatabaseConnection) UpdateRefcountFunction* GetUpdateRefcountFunction() const { AssertIsOnConnectionThread(); return mUpdateRefcountFunction; } nsresult BeginWriteTransaction(); nsresult CommitWriteTransaction(); void RollbackWriteTransaction(); void FinishWriteTransaction(); nsresult StartSavepoint(); nsresult ReleaseSavepoint(); nsresult RollbackSavepoint(); nsresult Checkpoint() { AssertIsOnConnectionThread(); return CheckpointInternal(CheckpointMode::Full); } void DoIdleProcessing(bool aNeedsCheckpoint); void Close(); nsresult DisableQuotaChecks(); void EnableQuotaChecks(); private: DatabaseConnection( MovingNotNull> aStorageConnection, MovingNotNull> aFileManager); ~DatabaseConnection(); nsresult Init(); nsresult CheckpointInternal(CheckpointMode aMode); Result GetFreelistCount( CachedStatement& aCachedStatement); /** * On success, returns whether some pages were freed. */ Result ReclaimFreePagesWhileIdle( CachedStatement& aFreelistStatement, CachedStatement& aRollbackStatement, uint32_t aFreelistCount, bool aNeedsCheckpoint); Result GetFileSize(const nsAString& aPath); }; class MOZ_STACK_CLASS DatabaseConnection::AutoSavepoint final { DatabaseConnection* mConnection; #ifdef DEBUG const TransactionBase* mDEBUGTransaction; #endif public: AutoSavepoint(); ~AutoSavepoint(); nsresult Start(const TransactionBase& aTransaction); nsresult Commit(); }; class DatabaseConnection::UpdateRefcountFunction final : public mozIStorageFunction { class FileInfoEntry; enum class UpdateType { Increment, Decrement }; DatabaseConnection* const mConnection; DatabaseFileManager& mFileManager; nsClassHashtable mFileInfoEntries; nsTHashMap> mSavepointEntriesIndex; nsTArray mJournalsToCreateBeforeCommit; nsTArray mJournalsToRemoveAfterCommit; nsTArray mJournalsToRemoveAfterAbort; bool mInSavepoint; public: NS_DECL_ISUPPORTS NS_DECL_MOZISTORAGEFUNCTION UpdateRefcountFunction(DatabaseConnection* aConnection, DatabaseFileManager& aFileManager); nsresult WillCommit(); void DidCommit(); void DidAbort(); void StartSavepoint(); void ReleaseSavepoint(); void RollbackSavepoint(); void Reset(); private: ~UpdateRefcountFunction() = default; nsresult ProcessValue(mozIStorageValueArray* aValues, int32_t aIndex, UpdateType aUpdateType); nsresult CreateJournals(); nsresult RemoveJournals(const nsTArray& aJournals); }; class DatabaseConnection::UpdateRefcountFunction::FileInfoEntry final { SafeRefPtr mFileInfo; int32_t mDelta; int32_t mSavepointDelta; public: explicit FileInfoEntry(SafeRefPtr aFileInfo) : mFileInfo(std::move(aFileInfo)), mDelta(0), mSavepointDelta(0) { MOZ_COUNT_CTOR(DatabaseConnection::UpdateRefcountFunction::FileInfoEntry); } void IncDeltas(bool aUpdateSavepointDelta) { ++mDelta; if (aUpdateSavepointDelta) { ++mSavepointDelta; } } void DecDeltas(bool aUpdateSavepointDelta) { --mDelta; if (aUpdateSavepointDelta) { --mSavepointDelta; } } void DecBySavepointDelta() { mDelta -= mSavepointDelta; } SafeRefPtr ReleaseFileInfo() { return std::move(mFileInfo); } void MaybeUpdateDBRefs() { if (mDelta) { mFileInfo->UpdateDBRefs(mDelta); } } int32_t Delta() const { return mDelta; } int32_t SavepointDelta() const { return mSavepointDelta; } ~FileInfoEntry() { MOZ_COUNT_DTOR(DatabaseConnection::UpdateRefcountFunction::FileInfoEntry); } }; class ConnectionPool final { public: class FinishCallback; private: class ConnectionRunnable; class CloseConnectionRunnable; struct DatabaseInfo; struct DatabaseCompleteCallback; class FinishCallbackWrapper; class IdleConnectionRunnable; class ThreadRunnable; class TransactionInfo; struct TransactionInfoPair; struct IdleResource { TimeStamp mIdleTime; IdleResource(const IdleResource& aOther) = delete; IdleResource(IdleResource&& aOther) noexcept : IdleResource(aOther.mIdleTime) {} IdleResource& operator=(const IdleResource& aOther) = delete; IdleResource& operator=(IdleResource&& aOther) = delete; protected: explicit IdleResource(const TimeStamp& aIdleTime); ~IdleResource(); }; struct IdleDatabaseInfo final : public IdleResource { InitializedOnce> mDatabaseInfo; public: explicit IdleDatabaseInfo(DatabaseInfo& aDatabaseInfo); IdleDatabaseInfo(const IdleDatabaseInfo& aOther) = delete; IdleDatabaseInfo(IdleDatabaseInfo&& aOther) noexcept : IdleResource(std::move(aOther)), mDatabaseInfo{std::move(aOther.mDatabaseInfo)} { MOZ_ASSERT(mDatabaseInfo); MOZ_COUNT_CTOR(ConnectionPool::IdleDatabaseInfo); } IdleDatabaseInfo& operator=(const IdleDatabaseInfo& aOther) = delete; IdleDatabaseInfo& operator=(IdleDatabaseInfo&& aOther) = delete; ~IdleDatabaseInfo(); bool operator==(const IdleDatabaseInfo& aOther) const { return *mDatabaseInfo == *aOther.mDatabaseInfo; } bool operator==(const DatabaseInfo* aDatabaseInfo) const { return *mDatabaseInfo == aDatabaseInfo; } bool operator<(const IdleDatabaseInfo& aOther) const { return mIdleTime < aOther.mIdleTime; } }; class ThreadInfo { public: ThreadInfo(); ThreadInfo(nsCOMPtr aThread, RefPtr aRunnable) : mThread{std::move(aThread)}, mRunnable{std::move(aRunnable)} { AssertIsOnBackgroundThread(); AssertValid(); MOZ_COUNT_CTOR(ConnectionPool::ThreadInfo); } ThreadInfo(const ThreadInfo& aOther) = delete; ThreadInfo& operator=(const ThreadInfo& aOther) = delete; ThreadInfo(ThreadInfo&& aOther) noexcept; ThreadInfo& operator=(ThreadInfo&& aOther) = default; bool IsValid() const { const bool res = mThread; if (res) { AssertValid(); } else { AssertEmpty(); } return res; } void AssertValid() const { MOZ_ASSERT(mThread); MOZ_ASSERT(mRunnable); } void AssertEmpty() const { MOZ_ASSERT(!mThread); MOZ_ASSERT(!mRunnable); } nsIThread& ThreadRef() { AssertValid(); return *mThread; } std::tuple, RefPtr> Forget() { AssertValid(); return {std::move(mThread), std::move(mRunnable)}; } ~ThreadInfo(); bool operator==(const ThreadInfo& aOther) const { return mThread == aOther.mThread && mRunnable == aOther.mRunnable; } private: nsCOMPtr mThread; RefPtr mRunnable; }; struct IdleThreadInfo final : public IdleResource { ThreadInfo mThreadInfo; explicit IdleThreadInfo(ThreadInfo aThreadInfo); IdleThreadInfo(const IdleThreadInfo& aOther) = delete; IdleThreadInfo(IdleThreadInfo&& aOther) noexcept : IdleResource(std::move(aOther)), mThreadInfo(std::move(aOther.mThreadInfo)) { AssertIsOnBackgroundThread(); mThreadInfo.AssertValid(); MOZ_COUNT_CTOR(ConnectionPool::IdleThreadInfo); } IdleThreadInfo& operator=(const IdleThreadInfo& aOther) = delete; IdleThreadInfo& operator=(IdleThreadInfo&& aOther) = delete; ~IdleThreadInfo(); bool operator==(const IdleThreadInfo& aOther) const { return mThreadInfo == aOther.mThreadInfo; } bool operator<(const IdleThreadInfo& aOther) const { return mIdleTime < aOther.mIdleTime; } }; // This mutex guards mDatabases, see below. Mutex mDatabasesMutex MOZ_UNANNOTATED; nsCOMPtr mIOTarget; nsTArray mIdleThreads; nsTArray mIdleDatabases; nsTArray> mDatabasesPerformingIdleMaintenance; nsCOMPtr mIdleTimer; TimeStamp mTargetIdleTime; // Only modifed on the owning thread, but read on multiple threads. Therefore // all modifications and all reads off the owning thread must be protected by // mDatabasesMutex. nsClassHashtable mDatabases; nsClassHashtable mTransactions; nsTArray> mQueuedTransactions; nsTArray> mCompleteCallbacks; uint64_t mNextTransactionId; uint32_t mTotalThreadCount; FlippedOnce mShutdownRequested; FlippedOnce mShutdownComplete; public: ConnectionPool(); void AssertIsOnOwningThread() const { NS_ASSERT_OWNINGTHREAD(ConnectionPool); } Result, nsresult> GetOrCreateConnection( const Database& aDatabase); uint64_t Start(const nsID& aBackgroundChildLoggingId, const nsACString& aDatabaseId, int64_t aLoggingSerialNumber, const nsTArray& aObjectStoreNames, bool aIsWriteTransaction, TransactionDatabaseOperationBase* aTransactionOp); void Dispatch(uint64_t aTransactionId, nsIRunnable* aRunnable); void Finish(uint64_t aTransactionId, FinishCallback* aCallback); void CloseDatabaseWhenIdle(const nsACString& aDatabaseId) { Unused << CloseDatabaseWhenIdleInternal(aDatabaseId); } void WaitForDatabaseToComplete(const nsCString& aDatabaseId, nsIRunnable* aCallback); void Shutdown(); NS_INLINE_DECL_REFCOUNTING(ConnectionPool) private: ~ConnectionPool(); static void IdleTimerCallback(nsITimer* aTimer, void* aClosure); static uint32_t SerialNumber() { return ++sSerialNumber; } static uint32_t sSerialNumber; void Cleanup(); void AdjustIdleTimer(); void CancelIdleTimer(); void ShutdownThread(ThreadInfo aThreadInfo); void CloseIdleDatabases(); void ShutdownIdleThreads(); bool ScheduleTransaction(TransactionInfo& aTransactionInfo, bool aFromQueuedTransactions); void NoteFinishedTransaction(uint64_t aTransactionId); void ScheduleQueuedTransactions(ThreadInfo aThreadInfo); void NoteIdleDatabase(DatabaseInfo& aDatabaseInfo); void NoteClosedDatabase(DatabaseInfo& aDatabaseInfo); bool MaybeFireCallback(DatabaseCompleteCallback* aCallback); void PerformIdleDatabaseMaintenance(DatabaseInfo& aDatabaseInfo); void CloseDatabase(DatabaseInfo& aDatabaseInfo) const; bool CloseDatabaseWhenIdleInternal(const nsACString& aDatabaseId); }; class ConnectionPool::ConnectionRunnable : public Runnable { protected: DatabaseInfo& mDatabaseInfo; nsCOMPtr mOwningEventTarget; explicit ConnectionRunnable(DatabaseInfo& aDatabaseInfo); ~ConnectionRunnable() override = default; }; class ConnectionPool::IdleConnectionRunnable final : public ConnectionRunnable { const bool mNeedsCheckpoint; public: IdleConnectionRunnable(DatabaseInfo& aDatabaseInfo, bool aNeedsCheckpoint) : ConnectionRunnable(aDatabaseInfo), mNeedsCheckpoint(aNeedsCheckpoint) {} NS_INLINE_DECL_REFCOUNTING_INHERITED(IdleConnectionRunnable, ConnectionRunnable) private: ~IdleConnectionRunnable() override = default; NS_DECL_NSIRUNNABLE }; class ConnectionPool::CloseConnectionRunnable final : public ConnectionRunnable { public: explicit CloseConnectionRunnable(DatabaseInfo& aDatabaseInfo) : ConnectionRunnable(aDatabaseInfo) {} NS_INLINE_DECL_REFCOUNTING_INHERITED(CloseConnectionRunnable, ConnectionRunnable) private: ~CloseConnectionRunnable() override = default; NS_DECL_NSIRUNNABLE }; struct ConnectionPool::DatabaseInfo final { friend class mozilla::DefaultDelete; RefPtr mConnectionPool; const nsCString mDatabaseId; RefPtr mConnection; nsClassHashtable mBlockingTransactions; nsTArray> mTransactionsScheduledDuringClose; nsTArray> mScheduledWriteTransactions; Maybe mRunningWriteTransaction; ThreadInfo mThreadInfo; uint32_t mReadTransactionCount; uint32_t mWriteTransactionCount; bool mNeedsCheckpoint; bool mIdle; FlippedOnce mCloseOnIdle; bool mClosing; #ifdef DEBUG nsISerialEventTarget* mDEBUGConnectionEventTarget; #endif DatabaseInfo(ConnectionPool* aConnectionPool, const nsACString& aDatabaseId); void AssertIsOnConnectionThread() const { MOZ_ASSERT(mDEBUGConnectionEventTarget); MOZ_ASSERT(GetCurrentSerialEventTarget() == mDEBUGConnectionEventTarget); } uint64_t TotalTransactionCount() const { return mReadTransactionCount + mWriteTransactionCount; } private: ~DatabaseInfo(); DatabaseInfo(const DatabaseInfo&) = delete; DatabaseInfo& operator=(const DatabaseInfo&) = delete; }; struct ConnectionPool::DatabaseCompleteCallback final { friend class DefaultDelete; nsCString mDatabaseId; nsCOMPtr mCallback; DatabaseCompleteCallback(const nsCString& aDatabaseIds, nsIRunnable* aCallback); private: ~DatabaseCompleteCallback(); }; class NS_NO_VTABLE ConnectionPool::FinishCallback : public nsIRunnable { public: // Called on the owning thread before any additional transactions are // unblocked. virtual void TransactionFinishedBeforeUnblock() = 0; // Called on the owning thread after additional transactions may have been // unblocked. virtual void TransactionFinishedAfterUnblock() = 0; protected: FinishCallback() = default; virtual ~FinishCallback() = default; }; class ConnectionPool::FinishCallbackWrapper final : public Runnable { RefPtr mConnectionPool; RefPtr mCallback; nsCOMPtr mOwningEventTarget; uint64_t mTransactionId; bool mHasRunOnce; public: FinishCallbackWrapper(ConnectionPool* aConnectionPool, uint64_t aTransactionId, FinishCallback* aCallback); NS_INLINE_DECL_REFCOUNTING_INHERITED(FinishCallbackWrapper, Runnable) private: ~FinishCallbackWrapper() override; NS_DECL_NSIRUNNABLE }; class ConnectionPool::ThreadRunnable final : public Runnable { // Set at construction for logging. const uint32_t mSerialNumber; // These two values are only modified on the connection thread. FlippedOnce mFirstRun; FlippedOnce mContinueRunning; public: explicit ThreadRunnable(uint32_t aSerialNumber); NS_INLINE_DECL_REFCOUNTING_INHERITED(ThreadRunnable, Runnable) uint32_t SerialNumber() const { return mSerialNumber; } private: ~ThreadRunnable() override; NS_DECL_NSIRUNNABLE }; class ConnectionPool::TransactionInfo final { friend class mozilla::DefaultDelete; nsTHashSet mBlocking; nsTArray> mBlockingOrdered; public: DatabaseInfo& mDatabaseInfo; const nsID mBackgroundChildLoggingId; const nsCString mDatabaseId; const uint64_t mTransactionId; const int64_t mLoggingSerialNumber; const nsTArray mObjectStoreNames; nsTHashSet mBlockedOn; nsTArray> mQueuedRunnables; const bool mIsWriteTransaction; bool mRunning; #ifdef DEBUG FlippedOnce mFinished; #endif TransactionInfo(DatabaseInfo& aDatabaseInfo, const nsID& aBackgroundChildLoggingId, const nsACString& aDatabaseId, uint64_t aTransactionId, int64_t aLoggingSerialNumber, const nsTArray& aObjectStoreNames, bool aIsWriteTransaction, TransactionDatabaseOperationBase* aTransactionOp); void AddBlockingTransaction(TransactionInfo& aTransactionInfo); void RemoveBlockingTransactions(); private: ~TransactionInfo(); void MaybeUnblock(TransactionInfo& aTransactionInfo); }; struct ConnectionPool::TransactionInfoPair final { // Multiple reading transactions can block future writes. nsTArray> mLastBlockingWrites; // But only a single writing transaction can block future reads. Maybe mLastBlockingReads; #if defined(DEBUG) || defined(NS_BUILD_REFCNT_LOGGING) TransactionInfoPair(); ~TransactionInfoPair(); #endif }; /******************************************************************************* * Actor class declarations ******************************************************************************/ template class CommonOpenOpHelper; template class IndexOpenOpHelper; template class ObjectStoreOpenOpHelper; template class OpenOpHelper; class DatabaseOperationBase : public Runnable, public mozIStorageProgressHandler { template friend class OpenOpHelper; protected: class AutoSetProgressHandler; using UniqueIndexTable = nsTHashMap; const nsCOMPtr mOwningEventTarget; const nsID mBackgroundChildLoggingId; const uint64_t mLoggingSerialNumber; private: nsresult mResultCode = NS_OK; Atomic mOperationMayProceed; FlippedOnce mActorDestroyed; public: NS_DECL_ISUPPORTS_INHERITED bool IsOnOwningThread() const { MOZ_ASSERT(mOwningEventTarget); bool current; return NS_SUCCEEDED(mOwningEventTarget->IsOnCurrentThread(¤t)) && current; } void AssertIsOnOwningThread() const { MOZ_ASSERT(IsOnBackgroundThread()); MOZ_ASSERT(IsOnOwningThread()); } void NoteActorDestroyed() { AssertIsOnOwningThread(); mActorDestroyed.EnsureFlipped(); mOperationMayProceed = false; } bool IsActorDestroyed() const { AssertIsOnOwningThread(); return mActorDestroyed; } // May be called on any thread, but you should call IsActorDestroyed() if // you know you're on the background thread because it is slightly faster. bool OperationMayProceed() const { return mOperationMayProceed; } const nsID& BackgroundChildLoggingId() const { return mBackgroundChildLoggingId; } uint64_t LoggingSerialNumber() const { return mLoggingSerialNumber; } nsresult ResultCode() const { return mResultCode; } void SetFailureCode(nsresult aFailureCode) { MOZ_ASSERT(NS_SUCCEEDED(mResultCode)); OverrideFailureCode(aFailureCode); } void SetFailureCodeIfUnset(nsresult aFailureCode) { if (NS_SUCCEEDED(mResultCode)) { OverrideFailureCode(aFailureCode); } } bool HasFailed() const { return NS_FAILED(mResultCode); } protected: DatabaseOperationBase(const nsID& aBackgroundChildLoggingId, uint64_t aLoggingSerialNumber) : Runnable("dom::indexedDB::DatabaseOperationBase"), mOwningEventTarget(GetCurrentSerialEventTarget()), mBackgroundChildLoggingId(aBackgroundChildLoggingId), mLoggingSerialNumber(aLoggingSerialNumber), mOperationMayProceed(true) { AssertIsOnOwningThread(); } ~DatabaseOperationBase() override { MOZ_ASSERT(mActorDestroyed); } void OverrideFailureCode(nsresult aFailureCode) { MOZ_ASSERT(NS_FAILED(aFailureCode)); mResultCode = aFailureCode; } static nsAutoCString MaybeGetBindingClauseForKeyRange( const Maybe& aOptionalKeyRange, const nsACString& aKeyColumnName); static nsAutoCString GetBindingClauseForKeyRange( const SerializedKeyRange& aKeyRange, const nsACString& aKeyColumnName); static uint64_t ReinterpretDoubleAsUInt64(double aDouble); static nsresult BindKeyRangeToStatement(const SerializedKeyRange& aKeyRange, mozIStorageStatement* aStatement); static nsresult BindKeyRangeToStatement(const SerializedKeyRange& aKeyRange, mozIStorageStatement* aStatement, const nsCString& aLocale); static Result IndexDataValuesFromUpdateInfos(const nsTArray& aUpdateInfos, const UniqueIndexTable& aUniqueIndexTable); static nsresult InsertIndexTableRows( DatabaseConnection* aConnection, IndexOrObjectStoreId aObjectStoreId, const Key& aObjectStoreKey, const nsTArray& aIndexValues); static nsresult DeleteIndexDataTableRows( DatabaseConnection* aConnection, const Key& aObjectStoreKey, const nsTArray& aIndexValues); static nsresult DeleteObjectStoreDataTableRowsWithIndexes( DatabaseConnection* aConnection, IndexOrObjectStoreId aObjectStoreId, const Maybe& aKeyRange); static nsresult UpdateIndexValues( DatabaseConnection* aConnection, IndexOrObjectStoreId aObjectStoreId, const Key& aObjectStoreKey, const nsTArray& aIndexValues); static Result ObjectStoreHasIndexes( DatabaseConnection& aConnection, IndexOrObjectStoreId aObjectStoreId); private: template static nsresult MaybeBindKeyToStatement( const Key& aKey, mozIStorageStatement* aStatement, const nsACString& aParameterName, const KeyTransformation& aKeyTransformation); template static nsresult BindTransformedKeyRangeToStatement( const SerializedKeyRange& aKeyRange, mozIStorageStatement* aStatement, const KeyTransformation& aKeyTransformation); // Not to be overridden by subclasses. NS_DECL_MOZISTORAGEPROGRESSHANDLER }; class MOZ_STACK_CLASS DatabaseOperationBase::AutoSetProgressHandler final { Maybe mConnection; #ifdef DEBUG DatabaseOperationBase* mDEBUGDatabaseOp; #endif public: AutoSetProgressHandler(); ~AutoSetProgressHandler(); nsresult Register(mozIStorageConnection& aConnection, DatabaseOperationBase* aDatabaseOp); void Unregister(); }; class TransactionDatabaseOperationBase : public DatabaseOperationBase { enum class InternalState { Initial, DatabaseWork, SendingPreprocess, WaitingForContinue, SendingResults, Completed }; InitializedOnce>> mTransaction; InternalState mInternalState = InternalState::Initial; bool mWaitingForContinue = false; const bool mTransactionIsAborted; protected: const int64_t mTransactionLoggingSerialNumber; #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED protected: // A check only enables when the diagnostic assert turns on. It assumes the // mUpdateRefcountFunction is a nullptr because the previous // StartTransactionOp failed on the connection thread and the next write // operation (e.g. ObjectstoreAddOrPutRequestOp) doesn't have enough time to // catch up the failure information. bool mAssumingPreviousOperationFail = false; #endif public: void AssertIsOnConnectionThread() const #ifdef DEBUG ; #else { } #endif uint64_t StartOnConnectionPool(const nsID& aBackgroundChildLoggingId, const nsACString& aDatabaseId, int64_t aLoggingSerialNumber, const nsTArray& aObjectStoreNames, bool aIsWriteTransaction); void DispatchToConnectionPool(); TransactionBase& Transaction() { return **mTransaction; } const TransactionBase& Transaction() const { return **mTransaction; } bool IsWaitingForContinue() const { AssertIsOnOwningThread(); return mWaitingForContinue; } void NoteContinueReceived(); int64_t TransactionLoggingSerialNumber() const { return mTransactionLoggingSerialNumber; } // May be overridden by subclasses if they need to perform work on the // background thread before being dispatched. Returning false will kill the // child actors and prevent dispatch. virtual bool Init(TransactionBase& aTransaction); // This callback will be called on the background thread before releasing the // final reference to this request object. Subclasses may perform any // additional cleanup here but must always call the base class implementation. virtual void Cleanup(); protected: explicit TransactionDatabaseOperationBase( SafeRefPtr aTransaction); TransactionDatabaseOperationBase(SafeRefPtr aTransaction, uint64_t aLoggingSerialNumber); ~TransactionDatabaseOperationBase() override; virtual void RunOnConnectionThread(); // Must be overridden in subclasses. Called on the target thread to allow the // subclass to perform necessary database or file operations. A successful // return value will trigger a SendSuccessResult callback on the background // thread while a failure value will trigger a SendFailureResult callback. virtual nsresult DoDatabaseWork(DatabaseConnection* aConnection) = 0; // May be overriden in subclasses. Called on the background thread to decide // if the subclass needs to send any preprocess info to the child actor. virtual bool HasPreprocessInfo(); // May be overriden in subclasses. Called on the background thread to allow // the subclass to serialize its preprocess info and send it to the child // actor. A successful return value will trigger a wait for a // NoteContinueReceived callback on the background thread while a failure // value will trigger a SendFailureResult callback. virtual nsresult SendPreprocessInfo(); // Must be overridden in subclasses. Called on the background thread to allow // the subclass to serialize its results and send them to the child actor. A // failed return value will trigger a SendFailureResult callback. virtual nsresult SendSuccessResult() = 0; // Must be overridden in subclasses. Called on the background thread to allow // the subclass to send its failure code. Returning false will cause the // transaction to be aborted with aResultCode. Returning true will not cause // the transaction to be aborted. virtual bool SendFailureResult(nsresult aResultCode) = 0; #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED auto MakeAutoSavepointCleanupHandler(DatabaseConnection& aConnection) { return [this, &aConnection](const auto) { if (!aConnection.GetUpdateRefcountFunction()) { mAssumingPreviousOperationFail = true; } }; } #endif private: void SendToConnectionPool(); void SendPreprocess(); void SendResults(); void SendPreprocessInfoOrResults(bool aSendPreprocessInfo); // Not to be overridden by subclasses. NS_DECL_NSIRUNNABLE }; class Factory final : public PBackgroundIDBFactoryParent, public AtomicSafeRefCounted { RefPtr mLoggingInfo; #ifdef DEBUG bool mActorDestroyed; #endif // Reference counted. ~Factory() override; public: [[nodiscard]] static SafeRefPtr Create( const LoggingInfo& aLoggingInfo); DatabaseLoggingInfo* GetLoggingInfo() const { AssertIsOnBackgroundThread(); MOZ_ASSERT(mLoggingInfo); return mLoggingInfo; } MOZ_DECLARE_REFCOUNTED_TYPENAME(mozilla::dom::indexedDB::Factory) MOZ_INLINE_DECL_SAFEREFCOUNTING_INHERITED(Factory, AtomicSafeRefCounted) // Only constructed in Create(). explicit Factory(RefPtr aLoggingInfo); // IPDL methods are only called by IPDL. void ActorDestroy(ActorDestroyReason aWhy) override; mozilla::ipc::IPCResult RecvDeleteMe() override; PBackgroundIDBFactoryRequestParent* AllocPBackgroundIDBFactoryRequestParent( const FactoryRequestParams& aParams) override; mozilla::ipc::IPCResult RecvPBackgroundIDBFactoryRequestConstructor( PBackgroundIDBFactoryRequestParent* aActor, const FactoryRequestParams& aParams) override; bool DeallocPBackgroundIDBFactoryRequestParent( PBackgroundIDBFactoryRequestParent* aActor) override; }; class WaitForTransactionsHelper final : public Runnable { const nsCString mDatabaseId; nsCOMPtr mCallback; enum class State { Initial = 0, WaitingForTransactions, Complete } mState; public: WaitForTransactionsHelper(const nsACString& aDatabaseId, nsIRunnable* aCallback) : Runnable("dom::indexedDB::WaitForTransactionsHelper"), mDatabaseId(aDatabaseId), mCallback(aCallback), mState(State::Initial) { AssertIsOnBackgroundThread(); MOZ_ASSERT(!aDatabaseId.IsEmpty()); MOZ_ASSERT(aCallback); } void WaitForTransactions(); NS_INLINE_DECL_REFCOUNTING_INHERITED(WaitForTransactionsHelper, Runnable) private: ~WaitForTransactionsHelper() override { MOZ_ASSERT(!mCallback); MOZ_ASSERT(mState == State::Complete); } void MaybeWaitForTransactions(); void CallCallback(); NS_DECL_NSIRUNNABLE }; class Database final : public PBackgroundIDBDatabaseParent, public SupportsCheckedUnsafePtr>, public AtomicSafeRefCounted { friend class VersionChangeTransaction; class StartTransactionOp; class UnmapBlobCallback; private: SafeRefPtr mFactory; SafeRefPtr mMetadata; SafeRefPtr mFileManager; RefPtr mDirectoryLock; nsTHashSet mTransactions; nsTHashMap> mMappedBlobs; RefPtr mConnection; const PrincipalInfo mPrincipalInfo; const Maybe mOptionalContentParentId; // XXX Consider changing this to ClientMetadata. const quota::OriginMetadata mOriginMetadata; const nsCString mId; const nsString mFilePath; const Maybe mKey; int64_t mDirectoryLockId; const uint32_t mTelemetryId; const PersistenceType mPersistenceType; const bool mInPrivateBrowsing; FlippedOnce mClosed; FlippedOnce mInvalidated; FlippedOnce mActorWasAlive; FlippedOnce mActorDestroyed; nsCOMPtr mBackgroundThread; #ifdef DEBUG bool mAllBlobsUnmapped; #endif public: // Created by OpenDatabaseOp. Database(SafeRefPtr aFactory, const PrincipalInfo& aPrincipalInfo, const Maybe& aOptionalContentParentId, const quota::OriginMetadata& aOriginMetadata, uint32_t aTelemetryId, SafeRefPtr aMetadata, SafeRefPtr aFileManager, RefPtr aDirectoryLock, bool aInPrivateBrowsing, const Maybe& aMaybeKey); void AssertIsOnConnectionThread() const { #ifdef DEBUG if (mConnection) { MOZ_ASSERT(mConnection); mConnection->AssertIsOnConnectionThread(); } else { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(mInvalidated); } #endif } NS_IMETHOD_(MozExternalRefCountType) AddRef() override { return AtomicSafeRefCounted::AddRef(); } NS_IMETHOD_(MozExternalRefCountType) Release() override { return AtomicSafeRefCounted::Release(); } MOZ_DECLARE_REFCOUNTED_TYPENAME(mozilla::dom::indexedDB::Database) void Invalidate(); bool IsOwnedByProcess(ContentParentId aContentParentId) const { return mOptionalContentParentId && mOptionalContentParentId.value() == aContentParentId; } const quota::OriginMetadata& OriginMetadata() const { return mOriginMetadata; } const nsCString& Id() const { return mId; } Maybe MaybeDirectoryLockRef() const { AssertIsOnBackgroundThread(); return ToMaybeRef(mDirectoryLock.get()); } int64_t DirectoryLockId() const { return mDirectoryLockId; } uint32_t TelemetryId() const { return mTelemetryId; } PersistenceType Type() const { return mPersistenceType; } const nsString& FilePath() const { return mFilePath; } DatabaseFileManager& GetFileManager() const { return *mFileManager; } MovingNotNull> GetFileManagerPtr() const { return WrapMovingNotNull(mFileManager.clonePtr()); } const FullDatabaseMetadata& Metadata() const { MOZ_ASSERT(mMetadata); return *mMetadata; } SafeRefPtr MetadataPtr() const { MOZ_ASSERT(mMetadata); return mMetadata.clonePtr(); } PBackgroundParent* GetBackgroundParent() const { AssertIsOnBackgroundThread(); MOZ_ASSERT(!IsActorDestroyed()); return Manager()->Manager(); } DatabaseLoggingInfo* GetLoggingInfo() const { AssertIsOnBackgroundThread(); MOZ_ASSERT(mFactory); return mFactory->GetLoggingInfo(); } bool RegisterTransaction(TransactionBase& aTransaction); void UnregisterTransaction(TransactionBase& aTransaction); void SetActorAlive(); void MapBlob(const IPCBlob& aIPCBlob, SafeRefPtr aFileInfo); bool IsActorAlive() const { AssertIsOnBackgroundThread(); return mActorWasAlive && !mActorDestroyed; } bool IsActorDestroyed() const { AssertIsOnBackgroundThread(); return mActorWasAlive && mActorDestroyed; } bool IsClosed() const { AssertIsOnBackgroundThread(); return mClosed; } bool IsInvalidated() const { AssertIsOnBackgroundThread(); return mInvalidated; } nsresult EnsureConnection(); DatabaseConnection* GetConnection() const { #ifdef DEBUG if (mConnection) { mConnection->AssertIsOnConnectionThread(); } #endif return mConnection; } void Stringify(nsACString& aResult) const; bool IsInPrivateBrowsing() const { AssertIsOnBackgroundThread(); return mInPrivateBrowsing; } const Maybe& MaybeKeyRef() const { // This can be called on any thread, as it is const. MOZ_ASSERT(mKey.isSome() == mInPrivateBrowsing); return mKey; } ~Database() override { MOZ_ASSERT(mClosed); MOZ_ASSERT_IF(mActorWasAlive, mActorDestroyed); NS_ProxyRelease("ReleaseIDBFactory", mBackgroundThread.get(), mFactory.forget()); } private: [[nodiscard]] SafeRefPtr GetBlob(const IPCBlob& aIPCBlob); void UnmapBlob(const nsID& aID); void UnmapAllBlobs(); bool CloseInternal(); void MaybeCloseConnection(); void ConnectionClosedCallback(); void CleanupMetadata(); // IPDL methods are only called by IPDL. void ActorDestroy(ActorDestroyReason aWhy) override; PBackgroundIDBDatabaseFileParent* AllocPBackgroundIDBDatabaseFileParent( const IPCBlob& aIPCBlob) override; bool DeallocPBackgroundIDBDatabaseFileParent( PBackgroundIDBDatabaseFileParent* aActor) override; already_AddRefed AllocPBackgroundIDBTransactionParent( const nsTArray& aObjectStoreNames, const Mode& aMode) override; mozilla::ipc::IPCResult RecvPBackgroundIDBTransactionConstructor( PBackgroundIDBTransactionParent* aActor, nsTArray&& aObjectStoreNames, const Mode& aMode) override; mozilla::ipc::IPCResult RecvDeleteMe() override; mozilla::ipc::IPCResult RecvBlocked() override; mozilla::ipc::IPCResult RecvClose() override; template static bool InvalidateAll(const nsTBaseHashSet>& aTable); }; class Database::StartTransactionOp final : public TransactionDatabaseOperationBase { friend class Database; private: explicit StartTransactionOp(SafeRefPtr aTransaction) : TransactionDatabaseOperationBase(std::move(aTransaction), /* aLoggingSerialNumber */ 0) {} ~StartTransactionOp() override = default; void RunOnConnectionThread() override; nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; nsresult SendSuccessResult() override; bool SendFailureResult(nsresult aResultCode) override; void Cleanup() override; }; class Database::UnmapBlobCallback final : public RemoteLazyInputStreamParentCallback { SafeRefPtr mDatabase; nsCOMPtr mBackgroundThread; public: explicit UnmapBlobCallback(SafeRefPtr aDatabase) : mDatabase(std::move(aDatabase)), mBackgroundThread(GetCurrentSerialEventTarget()) { AssertIsOnBackgroundThread(); } NS_INLINE_DECL_THREADSAFE_REFCOUNTING(Database::UnmapBlobCallback, override) void ActorDestroyed(const nsID& aID) override { MOZ_ASSERT(mDatabase); mBackgroundThread->Dispatch(NS_NewRunnableFunction( "UnmapBlobCallback", [aID, database = std::move(mDatabase)] { AssertIsOnBackgroundThread(); database->UnmapBlob(aID); })); } private: ~UnmapBlobCallback() = default; }; /** * In coordination with IDBDatabase's mFileActors weak-map on the child side, a * long-lived mapping from a child process's live Blobs to their corresponding * DatabaseFileInfo in our owning database. Assists in avoiding redundant IPC * traffic and disk storage. This includes both: * - Blobs retrieved from this database and sent to the child that do not need * to be written to disk because they already exist on disk in this database's * files directory. * - Blobs retrieved from other databases or from anywhere else that will need * to be written to this database's files directory. In this case we will * hold a reference to its BlobImpl in mBlobImpl until we have successfully * written the Blob to disk. * * Relevant Blob context: Blobs sent from the parent process to child processes * are automatically linked back to their source BlobImpl when the child process * references the Blob via IPC. This is done using the internal IPCBlob * inputStream actor ID to DatabaseFileInfo mapping. However, when getting an * actor in the child process for sending an in-child-created Blob to the * parent process, there is (currently) no Blob machinery to automatically * establish and reuse a long-lived Actor. As a result, without IDB's weak-map * cleverness, a memory-backed Blob repeatedly sent from the child to the parent * would appear as a different Blob each time, requiring the Blob data to be * sent over IPC each time as well as potentially needing to be written to disk * each time. * * This object remains alive as long as there is an active child actor or an * ObjectStoreAddOrPutRequestOp::StoredFileInfo for a queued or active add/put * op is holding a reference to us. */ class DatabaseFile final : public PBackgroundIDBDatabaseFileParent { // mBlobImpl's ownership lifecycle: // - Initialized on the background thread at creation time. Then // responsibility is handed off to the connection thread. // - Checked and used by the connection thread to generate a stream to write // the blob to disk by an add/put operation. // - Cleared on the connection thread once the file has successfully been // written to disk. InitializedOnce> mBlobImpl; const SafeRefPtr mFileInfo; public: NS_INLINE_DECL_THREADSAFE_REFCOUNTING(mozilla::dom::indexedDB::DatabaseFile); const DatabaseFileInfo& GetFileInfo() const { AssertIsOnBackgroundThread(); return *mFileInfo; } SafeRefPtr GetFileInfoPtr() const { AssertIsOnBackgroundThread(); return mFileInfo.clonePtr(); } /** * If mBlobImpl is non-null (implying the contents of this file have not yet * been written to disk), then return an input stream. Otherwise, if mBlobImpl * is null (because the contents have been written to disk), returns null. */ [[nodiscard]] nsCOMPtr GetInputStream(ErrorResult& rv) const; /** * To be called upon successful copying of the stream GetInputStream() * returned so that we won't try and redundantly write the file to disk in the * future. This is a separate step from GetInputStream() because * the write could fail due to quota errors that happen now but that might * not happen in a future attempt. */ void WriteSucceededClearBlobImpl() { MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(*mBlobImpl); mBlobImpl.destroy(); } public: // Called when sending to the child. explicit DatabaseFile(SafeRefPtr aFileInfo) : mBlobImpl{nullptr}, mFileInfo(std::move(aFileInfo)) { AssertIsOnBackgroundThread(); MOZ_ASSERT(mFileInfo); } // Called when receiving from the child. DatabaseFile(RefPtr aBlobImpl, SafeRefPtr aFileInfo) : mBlobImpl(std::move(aBlobImpl)), mFileInfo(std::move(aFileInfo)) { AssertIsOnBackgroundThread(); MOZ_ASSERT(*mBlobImpl); MOZ_ASSERT(mFileInfo); } private: ~DatabaseFile() override = default; void ActorDestroy(ActorDestroyReason aWhy) override { AssertIsOnBackgroundThread(); } }; nsCOMPtr DatabaseFile::GetInputStream(ErrorResult& rv) const { // We should only be called from our DB connection thread, not the background // thread. MOZ_ASSERT(!IsOnBackgroundThread()); // If we were constructed without a BlobImpl, or WriteSucceededClearBlobImpl // was already called, return nullptr. if (!mBlobImpl || !*mBlobImpl) { return nullptr; } nsCOMPtr inputStream; (*mBlobImpl)->CreateInputStream(getter_AddRefs(inputStream), rv); if (rv.Failed()) { return nullptr; } return inputStream; } class TransactionBase : public AtomicSafeRefCounted { friend class CursorBase; template friend class Cursor; class CommitOp; protected: using Mode = IDBTransaction::Mode; private: const SafeRefPtr mDatabase; nsTArray> mModifiedAutoIncrementObjectStoreMetadataArray; LazyInitializedOnceNotNull mTransactionId; const nsCString mDatabaseId; const int64_t mLoggingSerialNumber; uint64_t mActiveRequestCount; Atomic mInvalidatedOnAnyThread; const Mode mMode; FlippedOnce mInitialized; FlippedOnce mHasBeenActiveOnConnectionThread; FlippedOnce mActorDestroyed; FlippedOnce mInvalidated; protected: nsresult mResultCode; FlippedOnce mCommitOrAbortReceived; FlippedOnce mCommittedOrAborted; FlippedOnce mForceAborted; LazyInitializedOnce> mLastRequestBeforeCommit; Maybe mLastFailedRequest; public: void AssertIsOnConnectionThread() const { MOZ_ASSERT(mDatabase); mDatabase->AssertIsOnConnectionThread(); } bool IsActorDestroyed() const { AssertIsOnBackgroundThread(); return mActorDestroyed; } // Must be called on the background thread. bool IsInvalidated() const { MOZ_ASSERT(IsOnBackgroundThread(), "Use IsInvalidatedOnAnyThread()"); MOZ_ASSERT_IF(mInvalidated, NS_FAILED(mResultCode)); return mInvalidated; } // May be called on any thread, but is more expensive than IsInvalidated(). bool IsInvalidatedOnAnyThread() const { return mInvalidatedOnAnyThread; } void Init(const uint64_t aTransactionId) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aTransactionId); mTransactionId.init(aTransactionId); mInitialized.Flip(); } void SetActiveOnConnectionThread() { AssertIsOnConnectionThread(); mHasBeenActiveOnConnectionThread.Flip(); } MOZ_DECLARE_REFCOUNTED_TYPENAME(mozilla::dom::indexedDB::TransactionBase) void Abort(nsresult aResultCode, bool aForce); uint64_t TransactionId() const { return *mTransactionId; } const nsACString& DatabaseId() const { return mDatabaseId; } Mode GetMode() const { return mMode; } const Database& GetDatabase() const { MOZ_ASSERT(mDatabase); return *mDatabase; } Database& GetMutableDatabase() const { MOZ_ASSERT(mDatabase); return *mDatabase; } SafeRefPtr GetDatabasePtr() const { MOZ_ASSERT(mDatabase); return mDatabase.clonePtr(); } DatabaseLoggingInfo* GetLoggingInfo() const { AssertIsOnBackgroundThread(); MOZ_ASSERT(mDatabase); return mDatabase->GetLoggingInfo(); } int64_t LoggingSerialNumber() const { return mLoggingSerialNumber; } bool IsAborted() const { AssertIsOnBackgroundThread(); return NS_FAILED(mResultCode); } [[nodiscard]] SafeRefPtr GetMetadataForObjectStoreId( IndexOrObjectStoreId aObjectStoreId) const; [[nodiscard]] SafeRefPtr GetMetadataForIndexId( FullObjectStoreMetadata& aObjectStoreMetadata, IndexOrObjectStoreId aIndexId) const; PBackgroundParent* GetBackgroundParent() const { AssertIsOnBackgroundThread(); MOZ_ASSERT(!IsActorDestroyed()); return GetDatabase().GetBackgroundParent(); } void NoteModifiedAutoIncrementObjectStore( const SafeRefPtr& aMetadata); void ForgetModifiedAutoIncrementObjectStore( FullObjectStoreMetadata& aMetadata); void NoteActiveRequest(); void NoteFinishedRequest(int64_t aRequestId, nsresult aResultCode); void Invalidate(); virtual ~TransactionBase(); protected: TransactionBase(SafeRefPtr aDatabase, Mode aMode); void NoteActorDestroyed() { AssertIsOnBackgroundThread(); mActorDestroyed.Flip(); } #ifdef DEBUG // Only called by VersionChangeTransaction. void FakeActorDestroyed() { mActorDestroyed.EnsureFlipped(); } #endif mozilla::ipc::IPCResult RecvCommit(IProtocol* aActor, const Maybe aLastRequest); mozilla::ipc::IPCResult RecvAbort(IProtocol* aActor, nsresult aResultCode); void MaybeCommitOrAbort() { AssertIsOnBackgroundThread(); // If we've already committed or aborted then there's nothing else to do. if (mCommittedOrAborted) { return; } // If there are active requests then we have to wait for those requests to // complete (see NoteFinishedRequest). if (mActiveRequestCount) { return; } // If we haven't yet received a commit or abort message then there could be // additional requests coming so we should wait unless we're being forced to // abort. if (!mCommitOrAbortReceived && !mForceAborted) { return; } CommitOrAbort(); } PBackgroundIDBRequestParent* AllocRequest(RequestParams&& aParams, bool aTrustParams); bool StartRequest(PBackgroundIDBRequestParent* aActor); bool DeallocRequest(PBackgroundIDBRequestParent* aActor); already_AddRefed AllocCursor( const OpenCursorParams& aParams, bool aTrustParams); bool StartCursor(PBackgroundIDBCursorParent* aActor, const OpenCursorParams& aParams); virtual void UpdateMetadata(nsresult aResult) {} virtual void SendCompleteNotification(nsresult aResult) = 0; private: bool VerifyRequestParams(const RequestParams& aParams) const; bool VerifyRequestParams(const SerializedKeyRange& aParams) const; bool VerifyRequestParams(const ObjectStoreAddPutParams& aParams) const; bool VerifyRequestParams(const Maybe& aParams) const; void CommitOrAbort(); }; class TransactionBase::CommitOp final : public DatabaseOperationBase, public ConnectionPool::FinishCallback { friend class TransactionBase; SafeRefPtr mTransaction; nsresult mResultCode; ///< TODO: There is also a mResultCode in ///< DatabaseOperationBase. Is there a reason not to ///< use that? At least a more specific name should be ///< given to this one. private: CommitOp(SafeRefPtr aTransaction, nsresult aResultCode); ~CommitOp() override = default; // Writes new autoIncrement counts to database. nsresult WriteAutoIncrementCounts(); // Updates counts after a database activity has finished. void CommitOrRollbackAutoIncrementCounts(); void AssertForeignKeyConsistency(DatabaseConnection* aConnection) #ifdef DEBUG ; #else { } #endif NS_DECL_NSIRUNNABLE void TransactionFinishedBeforeUnblock() override; void TransactionFinishedAfterUnblock() override; public: // We need to declare all of nsISupports, because FinishCallback has // a pure-virtual nsISupports declaration. NS_DECL_ISUPPORTS_INHERITED }; class NormalTransaction final : public TransactionBase, public PBackgroundIDBTransactionParent { nsTArray> mObjectStores; // Reference counted. ~NormalTransaction() override = default; bool IsSameProcessActor(); // Only called by TransactionBase. void SendCompleteNotification(nsresult aResult) override; // IPDL methods are only called by IPDL. void ActorDestroy(ActorDestroyReason aWhy) override; mozilla::ipc::IPCResult RecvDeleteMe() override; mozilla::ipc::IPCResult RecvCommit( const Maybe& aLastRequest) override; mozilla::ipc::IPCResult RecvAbort(const nsresult& aResultCode) override; PBackgroundIDBRequestParent* AllocPBackgroundIDBRequestParent( const RequestParams& aParams) override; mozilla::ipc::IPCResult RecvPBackgroundIDBRequestConstructor( PBackgroundIDBRequestParent* aActor, const RequestParams& aParams) override; bool DeallocPBackgroundIDBRequestParent( PBackgroundIDBRequestParent* aActor) override; already_AddRefed AllocPBackgroundIDBCursorParent( const OpenCursorParams& aParams) override; mozilla::ipc::IPCResult RecvPBackgroundIDBCursorConstructor( PBackgroundIDBCursorParent* aActor, const OpenCursorParams& aParams) override; public: // This constructor is only called by Database. NormalTransaction( SafeRefPtr aDatabase, TransactionBase::Mode aMode, nsTArray>&& aObjectStores); MOZ_INLINE_DECL_SAFEREFCOUNTING_INHERITED(NormalTransaction, TransactionBase) }; class VersionChangeTransaction final : public TransactionBase, public PBackgroundIDBVersionChangeTransactionParent { friend class OpenDatabaseOp; RefPtr mOpenDatabaseOp; SafeRefPtr mOldMetadata; FlippedOnce mActorWasAlive; public: // Only called by OpenDatabaseOp. explicit VersionChangeTransaction(OpenDatabaseOp* aOpenDatabaseOp); MOZ_INLINE_DECL_SAFEREFCOUNTING_INHERITED(VersionChangeTransaction, TransactionBase) private: // Reference counted. ~VersionChangeTransaction() override; bool IsSameProcessActor(); // Only called by OpenDatabaseOp. bool CopyDatabaseMetadata(); void SetActorAlive(); // Only called by TransactionBase. void UpdateMetadata(nsresult aResult) override; // Only called by TransactionBase. void SendCompleteNotification(nsresult aResult) override; // IPDL methods are only called by IPDL. void ActorDestroy(ActorDestroyReason aWhy) override; mozilla::ipc::IPCResult RecvDeleteMe() override; mozilla::ipc::IPCResult RecvCommit( const Maybe& aLastRequest) override; mozilla::ipc::IPCResult RecvAbort(const nsresult& aResultCode) override; mozilla::ipc::IPCResult RecvCreateObjectStore( const ObjectStoreMetadata& aMetadata) override; mozilla::ipc::IPCResult RecvDeleteObjectStore( const IndexOrObjectStoreId& aObjectStoreId) override; mozilla::ipc::IPCResult RecvRenameObjectStore( const IndexOrObjectStoreId& aObjectStoreId, const nsAString& aName) override; mozilla::ipc::IPCResult RecvCreateIndex( const IndexOrObjectStoreId& aObjectStoreId, const IndexMetadata& aMetadata) override; mozilla::ipc::IPCResult RecvDeleteIndex( const IndexOrObjectStoreId& aObjectStoreId, const IndexOrObjectStoreId& aIndexId) override; mozilla::ipc::IPCResult RecvRenameIndex( const IndexOrObjectStoreId& aObjectStoreId, const IndexOrObjectStoreId& aIndexId, const nsAString& aName) override; PBackgroundIDBRequestParent* AllocPBackgroundIDBRequestParent( const RequestParams& aParams) override; mozilla::ipc::IPCResult RecvPBackgroundIDBRequestConstructor( PBackgroundIDBRequestParent* aActor, const RequestParams& aParams) override; bool DeallocPBackgroundIDBRequestParent( PBackgroundIDBRequestParent* aActor) override; already_AddRefed AllocPBackgroundIDBCursorParent( const OpenCursorParams& aParams) override; mozilla::ipc::IPCResult RecvPBackgroundIDBCursorConstructor( PBackgroundIDBCursorParent* aActor, const OpenCursorParams& aParams) override; }; class FactoryOp : public DatabaseOperationBase, public PBackgroundIDBFactoryRequestParent, public SupportsCheckedUnsafePtr> { public: struct MaybeBlockedDatabaseInfo final { SafeRefPtr mDatabase; bool mBlocked; MaybeBlockedDatabaseInfo(MaybeBlockedDatabaseInfo&&) = default; MaybeBlockedDatabaseInfo& operator=(MaybeBlockedDatabaseInfo&&) = default; MOZ_IMPLICIT MaybeBlockedDatabaseInfo(SafeRefPtr aDatabase) : mDatabase(std::move(aDatabase)), mBlocked(false) { MOZ_ASSERT(mDatabase); MOZ_COUNT_CTOR(FactoryOp::MaybeBlockedDatabaseInfo); } ~MaybeBlockedDatabaseInfo() { MOZ_COUNT_DTOR(FactoryOp::MaybeBlockedDatabaseInfo); } bool operator==(const Database* aOther) const { return mDatabase == aOther; } Database* operator->() const& MOZ_NO_ADDREF_RELEASE_ON_RETURN { return mDatabase.unsafeGetRawPtr(); } }; protected: enum class State { // Just created on the PBackground thread, dispatched to the main thread. // Next step is either SendingResults if permission is denied, // PermissionChallenge if the permission is unknown, or FinishOpen // if permission is granted. Initial, // Ensuring quota manager is created and opening directory on the // PBackground thread. Next step is either SendingResults if quota manager // is not available or DirectoryOpenPending if quota manager is available. FinishOpen, // Waiting for directory open allowed on the PBackground thread. The next // step is either SendingResults if directory lock failed to acquire, or // DatabaseOpenPending if directory lock is acquired. DirectoryOpenPending, // Waiting for database open allowed on the PBackground thread. The next // step is DatabaseWorkOpen. DatabaseOpenPending, // Waiting to do/doing work on the QuotaManager IO thread. Its next step is // either BeginVersionChange if the requested version doesn't match the // existing database version or SendingResults if the versions match. DatabaseWorkOpen, // Starting a version change transaction or deleting a database on the // PBackground thread. We need to notify other databases that a version // change is about to happen, and maybe tell the request that a version // change has been blocked. If databases are notified then the next step is // WaitingForOtherDatabasesToClose. Otherwise the next step is // WaitingForTransactionsToComplete. BeginVersionChange, // Waiting for other databases to close on the PBackground thread. This // state may persist until all databases are closed. The next state is // WaitingForTransactionsToComplete. WaitingForOtherDatabasesToClose, // Waiting for all transactions that could interfere with this operation to // complete on the PBackground thread. Next state is // DatabaseWorkVersionChange. WaitingForTransactionsToComplete, // Waiting to do/doing work on the "work thread". This involves waiting for // the VersionChangeOp (OpenDatabaseOp and DeleteDatabaseOp each have a // different implementation) to do its work. Eventually the state will // transition to SendingResults. DatabaseWorkVersionChange, // Waiting to send/sending results on the PBackground thread. Next step is // Completed. SendingResults, // All done. Completed }; // Must be released on the background thread! SafeRefPtr mFactory; Maybe mContentParentId; // Must be released on the main thread! RefPtr mDirectoryLock; RefPtr mDelayedOp; nsTArray mMaybeBlockedDatabases; const CommonFactoryRequestParams mCommonParams; OriginMetadata mOriginMetadata; nsCString mDatabaseId; nsString mDatabaseFilePath; int64_t mDirectoryLockId; State mState; bool mWaitingForPermissionRetry; bool mEnforcingQuota; const bool mDeleting; FlippedOnce mInPrivateBrowsing; public: const nsACString& Origin() const { AssertIsOnOwningThread(); return mOriginMetadata.mOrigin; } bool DatabaseFilePathIsKnown() const { AssertIsOnOwningThread(); return !mDatabaseFilePath.IsEmpty(); } const nsAString& DatabaseFilePath() const { AssertIsOnOwningThread(); MOZ_ASSERT(!mDatabaseFilePath.IsEmpty()); return mDatabaseFilePath; } void NoteDatabaseBlocked(Database* aDatabase); void NoteDatabaseClosed(Database* aDatabase); #ifdef DEBUG bool HasBlockedDatabases() const { return !mMaybeBlockedDatabases.IsEmpty(); } #endif void StringifyState(nsACString& aResult) const; void Stringify(nsACString& aResult) const; protected: FactoryOp(SafeRefPtr aFactory, const Maybe& aContentParentId, const CommonFactoryRequestParams& aCommonParams, bool aDeleting); ~FactoryOp() override { // Normally this would be out-of-line since it is a virtual function but // MSVC 2010 fails to link for some reason if it is not inlined here... MOZ_ASSERT_IF(OperationMayProceed(), mState == State::Initial || mState == State::Completed); } nsresult Open(); nsresult DirectoryOpen(); nsresult SendToIOThread(); void WaitForTransactions(); void CleanupMetadata(); void FinishSendResults(); nsresult SendVersionChangeMessages(DatabaseActorInfo* aDatabaseActorInfo, Maybe aOpeningDatabase, uint64_t aOldVersion, const Maybe& aNewVersion); // Methods that subclasses must implement. virtual nsresult DatabaseOpen() = 0; virtual nsresult DoDatabaseWork() = 0; virtual nsresult BeginVersionChange() = 0; virtual bool AreActorsAlive() = 0; virtual nsresult DispatchToWorkThread() = 0; // Should only be called by Run(). virtual void SendResults() = 0; // Common nsIRunnable implementation that subclasses may not override. NS_IMETHOD Run() final; void DirectoryLockAcquired(DirectoryLock* aLock); void DirectoryLockFailed(); // IPDL methods. void ActorDestroy(ActorDestroyReason aWhy) override; virtual void SendBlockedNotification() = 0; private: mozilla::Result CheckPermission(); nsresult FinishOpen(); // Test whether this FactoryOp needs to wait for the given op. bool MustWaitFor(const FactoryOp& aExistingOp); }; class OpenDatabaseOp final : public FactoryOp { friend class Database; friend class VersionChangeTransaction; class VersionChangeOp; SafeRefPtr mMetadata; uint64_t mRequestedVersion; SafeRefPtr mFileManager; SafeRefPtr mDatabase; SafeRefPtr mVersionChangeTransaction; // This is only set while a VersionChangeOp is live. It holds a strong // reference to its OpenDatabaseOp object so this is a weak pointer to avoid // cycles. VersionChangeOp* mVersionChangeOp; uint32_t mTelemetryId; public: OpenDatabaseOp(SafeRefPtr aFactory, const Maybe& aContentParentId, const CommonFactoryRequestParams& aParams); private: ~OpenDatabaseOp() override { MOZ_ASSERT(!mVersionChangeOp); } nsresult LoadDatabaseInformation(mozIStorageConnection& aConnection); nsresult SendUpgradeNeeded(); void EnsureDatabaseActor(); nsresult EnsureDatabaseActorIsAlive(); mozilla::Result MetadataToSpec() const; void AssertMetadataConsistency(const FullDatabaseMetadata& aMetadata) #ifdef DEBUG ; #else { } #endif void ConnectionClosedCallback(); void ActorDestroy(ActorDestroyReason aWhy) override; nsresult DatabaseOpen() override; nsresult DoDatabaseWork() override; nsresult BeginVersionChange() override; bool AreActorsAlive() override; void SendBlockedNotification() override; nsresult DispatchToWorkThread() override; void SendResults() override; static nsresult UpdateLocaleAwareIndex(mozIStorageConnection& aConnection, const IndexMetadata& aIndexMetadata, const nsCString& aLocale); }; class OpenDatabaseOp::VersionChangeOp final : public TransactionDatabaseOperationBase { friend class OpenDatabaseOp; RefPtr mOpenDatabaseOp; const uint64_t mRequestedVersion; uint64_t mPreviousVersion; private: explicit VersionChangeOp(OpenDatabaseOp* aOpenDatabaseOp) : TransactionDatabaseOperationBase( aOpenDatabaseOp->mVersionChangeTransaction.clonePtr(), aOpenDatabaseOp->LoggingSerialNumber()), mOpenDatabaseOp(aOpenDatabaseOp), mRequestedVersion(aOpenDatabaseOp->mRequestedVersion), mPreviousVersion( aOpenDatabaseOp->mMetadata->mCommonMetadata.version()) { MOZ_ASSERT(aOpenDatabaseOp); MOZ_ASSERT(mRequestedVersion); } ~VersionChangeOp() override { MOZ_ASSERT(!mOpenDatabaseOp); } nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; nsresult SendSuccessResult() override; bool SendFailureResult(nsresult aResultCode) override; void Cleanup() override; }; class DeleteDatabaseOp final : public FactoryOp { class VersionChangeOp; nsString mDatabaseDirectoryPath; nsString mDatabaseFilenameBase; uint64_t mPreviousVersion; public: DeleteDatabaseOp(SafeRefPtr aFactory, const Maybe& aContentParentId, const CommonFactoryRequestParams& aParams) : FactoryOp(std::move(aFactory), aContentParentId, aParams, /* aDeleting */ true), mPreviousVersion(0) {} private: ~DeleteDatabaseOp() override = default; void LoadPreviousVersion(nsIFile& aDatabaseFile); nsresult DatabaseOpen() override; nsresult DoDatabaseWork() override; nsresult BeginVersionChange() override; bool AreActorsAlive() override; void SendBlockedNotification() override; nsresult DispatchToWorkThread() override; void SendResults() override; }; class DeleteDatabaseOp::VersionChangeOp final : public DatabaseOperationBase { friend class DeleteDatabaseOp; RefPtr mDeleteDatabaseOp; private: explicit VersionChangeOp(DeleteDatabaseOp* aDeleteDatabaseOp) : DatabaseOperationBase(aDeleteDatabaseOp->BackgroundChildLoggingId(), aDeleteDatabaseOp->LoggingSerialNumber()), mDeleteDatabaseOp(aDeleteDatabaseOp) { MOZ_ASSERT(aDeleteDatabaseOp); MOZ_ASSERT(!aDeleteDatabaseOp->mDatabaseDirectoryPath.IsEmpty()); } ~VersionChangeOp() override = default; nsresult RunOnIOThread(); void RunOnOwningThread(); NS_DECL_NSIRUNNABLE }; class VersionChangeTransactionOp : public TransactionDatabaseOperationBase { public: void Cleanup() override; protected: explicit VersionChangeTransactionOp( SafeRefPtr aTransaction) : TransactionDatabaseOperationBase(std::move(aTransaction)) {} ~VersionChangeTransactionOp() override = default; private: nsresult SendSuccessResult() override; bool SendFailureResult(nsresult aResultCode) override; }; class CreateObjectStoreOp final : public VersionChangeTransactionOp { friend class VersionChangeTransaction; const ObjectStoreMetadata mMetadata; private: // Only created by VersionChangeTransaction. CreateObjectStoreOp(SafeRefPtr aTransaction, const ObjectStoreMetadata& aMetadata) : VersionChangeTransactionOp(std::move(aTransaction)), mMetadata(aMetadata) { MOZ_ASSERT(aMetadata.id()); } ~CreateObjectStoreOp() override = default; nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; }; class DeleteObjectStoreOp final : public VersionChangeTransactionOp { friend class VersionChangeTransaction; const SafeRefPtr mMetadata; const bool mIsLastObjectStore; private: // Only created by VersionChangeTransaction. DeleteObjectStoreOp(SafeRefPtr aTransaction, SafeRefPtr aMetadata, const bool aIsLastObjectStore) : VersionChangeTransactionOp(std::move(aTransaction)), mMetadata(std::move(aMetadata)), mIsLastObjectStore(aIsLastObjectStore) { MOZ_ASSERT(mMetadata->mCommonMetadata.id()); } ~DeleteObjectStoreOp() override = default; nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; }; class RenameObjectStoreOp final : public VersionChangeTransactionOp { friend class VersionChangeTransaction; const int64_t mId; const nsString mNewName; private: // Only created by VersionChangeTransaction. RenameObjectStoreOp(SafeRefPtr aTransaction, FullObjectStoreMetadata& aMetadata) : VersionChangeTransactionOp(std::move(aTransaction)), mId(aMetadata.mCommonMetadata.id()), mNewName(aMetadata.mCommonMetadata.name()) { MOZ_ASSERT(mId); } ~RenameObjectStoreOp() override = default; nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; }; class CreateIndexOp final : public VersionChangeTransactionOp { friend class VersionChangeTransaction; class UpdateIndexDataValuesFunction; const IndexMetadata mMetadata; Maybe mMaybeUniqueIndexTable; const SafeRefPtr mFileManager; const nsCString mDatabaseId; const IndexOrObjectStoreId mObjectStoreId; private: // Only created by VersionChangeTransaction. CreateIndexOp(SafeRefPtr aTransaction, IndexOrObjectStoreId aObjectStoreId, const IndexMetadata& aMetadata); ~CreateIndexOp() override = default; nsresult InsertDataFromObjectStore(DatabaseConnection* aConnection); nsresult InsertDataFromObjectStoreInternal( DatabaseConnection* aConnection) const; bool Init(TransactionBase& aTransaction) override; nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; }; class CreateIndexOp::UpdateIndexDataValuesFunction final : public mozIStorageFunction { RefPtr mOp; RefPtr mConnection; const NotNull> mDatabase; public: UpdateIndexDataValuesFunction(CreateIndexOp* aOp, DatabaseConnection* aConnection, SafeRefPtr aDatabase) : mOp(aOp), mConnection(aConnection), mDatabase(WrapNotNull(std::move(aDatabase))) { MOZ_ASSERT(aOp); MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); } NS_DECL_ISUPPORTS private: ~UpdateIndexDataValuesFunction() = default; NS_DECL_MOZISTORAGEFUNCTION }; class DeleteIndexOp final : public VersionChangeTransactionOp { friend class VersionChangeTransaction; const IndexOrObjectStoreId mObjectStoreId; const IndexOrObjectStoreId mIndexId; const bool mUnique; const bool mIsLastIndex; private: // Only created by VersionChangeTransaction. DeleteIndexOp(SafeRefPtr aTransaction, IndexOrObjectStoreId aObjectStoreId, IndexOrObjectStoreId aIndexId, const bool aUnique, const bool aIsLastIndex); ~DeleteIndexOp() override = default; nsresult RemoveReferencesToIndex( DatabaseConnection* aConnection, const Key& aObjectDataKey, nsTArray& aIndexValues) const; nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; }; class RenameIndexOp final : public VersionChangeTransactionOp { friend class VersionChangeTransaction; const IndexOrObjectStoreId mObjectStoreId; const IndexOrObjectStoreId mIndexId; const nsString mNewName; private: // Only created by VersionChangeTransaction. RenameIndexOp(SafeRefPtr aTransaction, FullIndexMetadata& aMetadata, IndexOrObjectStoreId aObjectStoreId) : VersionChangeTransactionOp(std::move(aTransaction)), mObjectStoreId(aObjectStoreId), mIndexId(aMetadata.mCommonMetadata.id()), mNewName(aMetadata.mCommonMetadata.name()) { MOZ_ASSERT(mIndexId); } ~RenameIndexOp() override = default; nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; }; class NormalTransactionOp : public TransactionDatabaseOperationBase, public PBackgroundIDBRequestParent { #ifdef DEBUG bool mResponseSent; #endif public: void Cleanup() override; protected: explicit NormalTransactionOp(SafeRefPtr aTransaction) : TransactionDatabaseOperationBase(std::move(aTransaction)) #ifdef DEBUG , mResponseSent(false) #endif { } ~NormalTransactionOp() override = default; // An overload of DatabaseOperationBase's function that can avoid doing extra // work on non-versionchange transactions. mozilla::Result ObjectStoreHasIndexes( DatabaseConnection& aConnection, IndexOrObjectStoreId aObjectStoreId, bool aMayHaveIndexes); virtual mozilla::Result GetPreprocessParams(); // Subclasses use this override to set the IPDL response value. virtual void GetResponse(RequestResponse& aResponse, size_t* aResponseSize) = 0; private: nsresult SendPreprocessInfo() override; nsresult SendSuccessResult() override; bool SendFailureResult(nsresult aResultCode) override; // IPDL methods. void ActorDestroy(ActorDestroyReason aWhy) override; mozilla::ipc::IPCResult RecvContinue( const PreprocessResponse& aResponse) final; }; class ObjectStoreAddOrPutRequestOp final : public NormalTransactionOp { friend class TransactionBase; using PersistenceType = mozilla::dom::quota::PersistenceType; class StoredFileInfo final { InitializedOnce>> mFileInfo; // Either nothing, a file actor or a non-Blob-backed inputstream to write to // disk. using FileActorOrInputStream = Variant, nsCOMPtr>; InitializedOnce mFileActorOrInputStream; #ifdef DEBUG const StructuredCloneFileBase::FileType mType; #endif void EnsureCipherKey(); void AssertInvariants() const; StoredFileInfo(SafeRefPtr aFileInfo, RefPtr aFileActor); StoredFileInfo(SafeRefPtr aFileInfo, nsCOMPtr aInputStream); public: #if defined(NS_BUILD_REFCNT_LOGGING) // Only for MOZ_COUNT_CTOR. StoredFileInfo(StoredFileInfo&& aOther) : mFileInfo{std::move(aOther.mFileInfo)}, mFileActorOrInputStream{std::move(aOther.mFileActorOrInputStream)} # ifdef DEBUG , mType{aOther.mType} # endif { MOZ_COUNT_CTOR(ObjectStoreAddOrPutRequestOp::StoredFileInfo); } #else StoredFileInfo(StoredFileInfo&&) = default; #endif static StoredFileInfo CreateForBlob(SafeRefPtr aFileInfo, RefPtr aFileActor); static StoredFileInfo CreateForStructuredClone( SafeRefPtr aFileInfo, nsCOMPtr aInputStream); #if defined(DEBUG) || defined(NS_BUILD_REFCNT_LOGGING) ~StoredFileInfo() { AssertIsOnBackgroundThread(); AssertInvariants(); MOZ_COUNT_DTOR(ObjectStoreAddOrPutRequestOp::StoredFileInfo); } #endif bool IsValid() const { return static_cast(mFileInfo); } const DatabaseFileInfo& GetFileInfo() const { return **mFileInfo; } bool ShouldCompress() const; void NotifyWriteSucceeded() const; using InputStreamResult = mozilla::Result, nsresult>; InputStreamResult GetInputStream(); void Serialize(nsString& aText) const; }; class SCInputStream; const ObjectStoreAddPutParams mParams; Maybe mUniqueIndexTable; // This must be non-const so that we can update the mNextAutoIncrementId field // if we are modifying an autoIncrement objectStore. SafeRefPtr mMetadata; nsTArray mStoredFileInfos; Key mResponse; const OriginMetadata mOriginMetadata; const PersistenceType mPersistenceType; const bool mOverwrite; bool mObjectStoreMayHaveIndexes; bool mDataOverThreshold; private: // Only created by TransactionBase. ObjectStoreAddOrPutRequestOp(SafeRefPtr aTransaction, RequestParams&& aParams); ~ObjectStoreAddOrPutRequestOp() override = default; nsresult RemoveOldIndexDataValues(DatabaseConnection* aConnection); bool Init(TransactionBase& aTransaction) override; nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; void GetResponse(RequestResponse& aResponse, size_t* aResponseSize) override; void Cleanup() override; }; void ObjectStoreAddOrPutRequestOp::StoredFileInfo::AssertInvariants() const { // The only allowed types are eStructuredClone, eBlob and eMutableFile. MOZ_ASSERT(StructuredCloneFileBase::eStructuredClone == mType || StructuredCloneFileBase::eBlob == mType || StructuredCloneFileBase::eMutableFile == mType); // mFileInfo and a file actor in mFileActorOrInputStream are present until // the object is moved away, but an inputStream in mFileActorOrInputStream // can be released early. MOZ_ASSERT_IF(static_cast(mFileActorOrInputStream) && mFileActorOrInputStream->is>(), static_cast(mFileInfo)); if (mFileInfo) { // In a non-moved StoredFileInfo, one of the following is true: // - This was an overflow structured clone (eStructuredClone) and // storedFileInfo.mFileActorOrInputStream CAN be a non-nullptr input // stream (but that might have been release by ReleaseInputStream). MOZ_ASSERT_IF( StructuredCloneFileBase::eStructuredClone == mType, !mFileActorOrInputStream || (mFileActorOrInputStream->is>() && mFileActorOrInputStream->as>())); // - This is a reference to a Blob (eBlob) that may or may not have // already been written to disk. storedFileInfo.mFileActorOrInputStream // MUST be a non-null file actor, but its GetInputStream may return // nullptr (so don't assert on that). MOZ_ASSERT_IF(StructuredCloneFileBase::eBlob == mType, mFileActorOrInputStream->is>() && mFileActorOrInputStream->as>()); // - It's a mutable file (eMutableFile). No writing will be performed, // and storedFileInfo.mFileActorOrInputStream is Nothing. MOZ_ASSERT_IF(StructuredCloneFileBase::eMutableFile == mType, mFileActorOrInputStream->is()); } } void ObjectStoreAddOrPutRequestOp::StoredFileInfo::EnsureCipherKey() { const auto& fileInfo = GetFileInfo(); const auto& fileManager = fileInfo.Manager(); // No need to generate cipher keys if we are not in PBM if (!fileManager.IsInPrivateBrowsingMode()) { return; } nsCString keyId; keyId.AppendInt(fileInfo.Id()); fileManager.MutableCipherKeyManagerRef().Ensure(keyId); } ObjectStoreAddOrPutRequestOp::StoredFileInfo::StoredFileInfo( SafeRefPtr aFileInfo, RefPtr aFileActor) : mFileInfo{WrapNotNull(std::move(aFileInfo))}, mFileActorOrInputStream{std::move(aFileActor)} #ifdef DEBUG , mType{StructuredCloneFileBase::eBlob} #endif { AssertIsOnBackgroundThread(); AssertInvariants(); EnsureCipherKey(); MOZ_COUNT_CTOR(ObjectStoreAddOrPutRequestOp::StoredFileInfo); } ObjectStoreAddOrPutRequestOp::StoredFileInfo::StoredFileInfo( SafeRefPtr aFileInfo, nsCOMPtr aInputStream) : mFileInfo{WrapNotNull(std::move(aFileInfo))}, mFileActorOrInputStream{std::move(aInputStream)} #ifdef DEBUG , mType{StructuredCloneFileBase::eStructuredClone} #endif { AssertIsOnBackgroundThread(); AssertInvariants(); EnsureCipherKey(); MOZ_COUNT_CTOR(ObjectStoreAddOrPutRequestOp::StoredFileInfo); } ObjectStoreAddOrPutRequestOp::StoredFileInfo ObjectStoreAddOrPutRequestOp::StoredFileInfo::CreateForBlob( SafeRefPtr aFileInfo, RefPtr aFileActor) { return {std::move(aFileInfo), std::move(aFileActor)}; } ObjectStoreAddOrPutRequestOp::StoredFileInfo ObjectStoreAddOrPutRequestOp::StoredFileInfo::CreateForStructuredClone( SafeRefPtr aFileInfo, nsCOMPtr aInputStream) { return {std::move(aFileInfo), std::move(aInputStream)}; } bool ObjectStoreAddOrPutRequestOp::StoredFileInfo::ShouldCompress() const { // Must not be called after moving. MOZ_ASSERT(IsValid()); // Compression is only necessary for eStructuredClone, i.e. when // mFileActorOrInputStream stored an input stream. However, this is only // called after GetInputStream, when mFileActorOrInputStream has been // cleared, which is only possible for this type. const bool res = !mFileActorOrInputStream; MOZ_ASSERT(res == (StructuredCloneFileBase::eStructuredClone == mType)); return res; } void ObjectStoreAddOrPutRequestOp::StoredFileInfo::NotifyWriteSucceeded() const { MOZ_ASSERT(IsValid()); // For eBlob, clear the blob implementation. if (mFileActorOrInputStream && mFileActorOrInputStream->is>()) { mFileActorOrInputStream->as>() ->WriteSucceededClearBlobImpl(); } // For the other types, no action is necessary. } ObjectStoreAddOrPutRequestOp::StoredFileInfo::InputStreamResult ObjectStoreAddOrPutRequestOp::StoredFileInfo::GetInputStream() { if (!mFileActorOrInputStream) { MOZ_ASSERT(StructuredCloneFileBase::eStructuredClone == mType); return nsCOMPtr{}; } // For the different cases, see also the comments in AssertInvariants. return mFileActorOrInputStream->match( [](const Nothing&) -> InputStreamResult { return nsCOMPtr{}; }, [](const RefPtr& databaseActor) -> InputStreamResult { ErrorResult rv; auto inputStream = databaseActor->GetInputStream(rv); if (NS_WARN_IF(rv.Failed())) { return Err(rv.StealNSResult()); } return inputStream; }, [this](const nsCOMPtr& inputStream) -> InputStreamResult { auto res = inputStream; // destroy() clears the inputStream parameter, so we needed to make a // copy before mFileActorOrInputStream.destroy(); AssertInvariants(); return res; }); } void ObjectStoreAddOrPutRequestOp::StoredFileInfo::Serialize( nsString& aText) const { AssertInvariants(); MOZ_ASSERT(IsValid()); const int64_t id = (*mFileInfo)->Id(); auto structuredCloneHandler = [&aText, id](const nsCOMPtr&) { // eStructuredClone aText.Append('.'); aText.AppendInt(id); }; // If mFileActorOrInputStream was moved, we had an inputStream before. if (!mFileActorOrInputStream) { structuredCloneHandler(nullptr); return; } // This encoding is parsed in DeserializeStructuredCloneFile. mFileActorOrInputStream->match( [&aText, id](const Nothing&) { // eMutableFile aText.AppendInt(-id); }, [&aText, id](const RefPtr&) { // eBlob aText.AppendInt(id); }, structuredCloneHandler); } class ObjectStoreAddOrPutRequestOp::SCInputStream final : public nsIInputStream { const JSStructuredCloneData& mData; JSStructuredCloneData::Iterator mIter; public: explicit SCInputStream(const JSStructuredCloneData& aData) : mData(aData), mIter(aData.Start()) {} private: virtual ~SCInputStream() = default; NS_DECL_THREADSAFE_ISUPPORTS NS_DECL_NSIINPUTSTREAM }; class ObjectStoreGetRequestOp final : public NormalTransactionOp { friend class TransactionBase; const IndexOrObjectStoreId mObjectStoreId; SafeRefPtr mDatabase; const Maybe mOptionalKeyRange; AutoTArray mResponse; PBackgroundParent* mBackgroundParent; uint32_t mPreprocessInfoCount; const uint32_t mLimit; const bool mGetAll; private: // Only created by TransactionBase. ObjectStoreGetRequestOp(SafeRefPtr aTransaction, const RequestParams& aParams, bool aGetAll); ~ObjectStoreGetRequestOp() override = default; template mozilla::Result ConvertResponse( StructuredCloneReadInfoParent&& aInfo); nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; bool HasPreprocessInfo() override; mozilla::Result GetPreprocessParams() override; void GetResponse(RequestResponse& aResponse, size_t* aResponseSize) override; }; class ObjectStoreGetKeyRequestOp final : public NormalTransactionOp { friend class TransactionBase; const IndexOrObjectStoreId mObjectStoreId; const Maybe mOptionalKeyRange; const uint32_t mLimit; const bool mGetAll; nsTArray mResponse; private: // Only created by TransactionBase. ObjectStoreGetKeyRequestOp(SafeRefPtr aTransaction, const RequestParams& aParams, bool aGetAll); ~ObjectStoreGetKeyRequestOp() override = default; nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; void GetResponse(RequestResponse& aResponse, size_t* aResponseSize) override; }; class ObjectStoreDeleteRequestOp final : public NormalTransactionOp { friend class TransactionBase; const ObjectStoreDeleteParams mParams; ObjectStoreDeleteResponse mResponse; bool mObjectStoreMayHaveIndexes; private: ObjectStoreDeleteRequestOp(SafeRefPtr aTransaction, const ObjectStoreDeleteParams& aParams); ~ObjectStoreDeleteRequestOp() override = default; nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; void GetResponse(RequestResponse& aResponse, size_t* aResponseSize) override { aResponse = std::move(mResponse); *aResponseSize = 0; } }; class ObjectStoreClearRequestOp final : public NormalTransactionOp { friend class TransactionBase; const ObjectStoreClearParams mParams; ObjectStoreClearResponse mResponse; bool mObjectStoreMayHaveIndexes; private: ObjectStoreClearRequestOp(SafeRefPtr aTransaction, const ObjectStoreClearParams& aParams); ~ObjectStoreClearRequestOp() override = default; nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; void GetResponse(RequestResponse& aResponse, size_t* aResponseSize) override { aResponse = std::move(mResponse); *aResponseSize = 0; } }; class ObjectStoreCountRequestOp final : public NormalTransactionOp { friend class TransactionBase; const ObjectStoreCountParams mParams; ObjectStoreCountResponse mResponse; private: ObjectStoreCountRequestOp(SafeRefPtr aTransaction, const ObjectStoreCountParams& aParams) : NormalTransactionOp(std::move(aTransaction)), mParams(aParams) {} ~ObjectStoreCountRequestOp() override = default; nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; void GetResponse(RequestResponse& aResponse, size_t* aResponseSize) override { aResponse = std::move(mResponse); *aResponseSize = sizeof(uint64_t); } }; class IndexRequestOpBase : public NormalTransactionOp { protected: const SafeRefPtr mMetadata; protected: IndexRequestOpBase(SafeRefPtr aTransaction, const RequestParams& aParams) : NormalTransactionOp(std::move(aTransaction)), mMetadata(IndexMetadataForParams(Transaction(), aParams)) {} ~IndexRequestOpBase() override = default; private: static SafeRefPtr IndexMetadataForParams( const TransactionBase& aTransaction, const RequestParams& aParams); }; class IndexGetRequestOp final : public IndexRequestOpBase { friend class TransactionBase; SafeRefPtr mDatabase; const Maybe mOptionalKeyRange; AutoTArray mResponse; PBackgroundParent* mBackgroundParent; const uint32_t mLimit; const bool mGetAll; private: // Only created by TransactionBase. IndexGetRequestOp(SafeRefPtr aTransaction, const RequestParams& aParams, bool aGetAll); ~IndexGetRequestOp() override = default; nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; void GetResponse(RequestResponse& aResponse, size_t* aResponseSize) override; }; class IndexGetKeyRequestOp final : public IndexRequestOpBase { friend class TransactionBase; const Maybe mOptionalKeyRange; AutoTArray mResponse; const uint32_t mLimit; const bool mGetAll; private: // Only created by TransactionBase. IndexGetKeyRequestOp(SafeRefPtr aTransaction, const RequestParams& aParams, bool aGetAll); ~IndexGetKeyRequestOp() override = default; nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; void GetResponse(RequestResponse& aResponse, size_t* aResponseSize) override; }; class IndexCountRequestOp final : public IndexRequestOpBase { friend class TransactionBase; const IndexCountParams mParams; IndexCountResponse mResponse; private: // Only created by TransactionBase. IndexCountRequestOp(SafeRefPtr aTransaction, const RequestParams& aParams) : IndexRequestOpBase(std::move(aTransaction), aParams), mParams(aParams.get_IndexCountParams()) {} ~IndexCountRequestOp() override = default; nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; void GetResponse(RequestResponse& aResponse, size_t* aResponseSize) override { aResponse = std::move(mResponse); *aResponseSize = sizeof(uint64_t); } }; template class Cursor; constexpr IDBCursorType ToKeyOnlyType(const IDBCursorType aType) { MOZ_ASSERT(aType == IDBCursorType::ObjectStore || aType == IDBCursorType::ObjectStoreKey || aType == IDBCursorType::Index || aType == IDBCursorType::IndexKey); switch (aType) { case IDBCursorType::ObjectStore: [[fallthrough]]; case IDBCursorType::ObjectStoreKey: return IDBCursorType::ObjectStoreKey; case IDBCursorType::Index: [[fallthrough]]; case IDBCursorType::IndexKey: return IDBCursorType::IndexKey; } } template using CursorPosition = CursorData; #ifdef DEBUG constexpr indexedDB::OpenCursorParams::Type ToOpenCursorParamsType( const IDBCursorType aType) { MOZ_ASSERT(aType == IDBCursorType::ObjectStore || aType == IDBCursorType::ObjectStoreKey || aType == IDBCursorType::Index || aType == IDBCursorType::IndexKey); switch (aType) { case IDBCursorType::ObjectStore: return indexedDB::OpenCursorParams::TObjectStoreOpenCursorParams; case IDBCursorType::ObjectStoreKey: return indexedDB::OpenCursorParams::TObjectStoreOpenKeyCursorParams; case IDBCursorType::Index: return indexedDB::OpenCursorParams::TIndexOpenCursorParams; case IDBCursorType::IndexKey: return indexedDB::OpenCursorParams::TIndexOpenKeyCursorParams; } } #endif class CursorBase : public PBackgroundIDBCursorParent { friend class TransactionBase; template friend class CommonOpenOpHelper; protected: const SafeRefPtr mTransaction; // This should only be touched on the PBackground thread to check whether // the objectStore has been deleted. Holding these saves a hash lookup for // every call to continue()/advance(). InitializedOnce>> mObjectStoreMetadata; const IndexOrObjectStoreId mObjectStoreId; LazyInitializedOnce mLocaleAwareRangeBound; ///< If the cursor is based on a key range, the ///< bound in the direction of iteration (e.g. ///< the upper bound in case of mDirection == ///< NEXT). If the cursor is based on a key, it ///< is unset. If mLocale is set, this was ///< converted to mLocale. const Direction mDirection; const int32_t mMaxExtraCount; const bool mIsSameProcessActor; struct ConstructFromTransactionBase {}; public: NS_INLINE_DECL_THREADSAFE_REFCOUNTING(mozilla::dom::indexedDB::CursorBase, final) CursorBase(SafeRefPtr aTransaction, SafeRefPtr aObjectStoreMetadata, Direction aDirection, ConstructFromTransactionBase aConstructionTag); protected: // Reference counted. ~CursorBase() override { MOZ_ASSERT(!mObjectStoreMetadata); } private: virtual bool Start(const OpenCursorParams& aParams) = 0; }; class IndexCursorBase : public CursorBase { public: bool IsLocaleAware() const { return !mLocale.IsEmpty(); } IndexCursorBase(SafeRefPtr aTransaction, SafeRefPtr aObjectStoreMetadata, SafeRefPtr aIndexMetadata, Direction aDirection, ConstructFromTransactionBase aConstructionTag) : CursorBase{std::move(aTransaction), std::move(aObjectStoreMetadata), aDirection, aConstructionTag}, mIndexMetadata(WrapNotNull(std::move(aIndexMetadata))), mIndexId((*mIndexMetadata)->mCommonMetadata.id()), mUniqueIndex((*mIndexMetadata)->mCommonMetadata.unique()), mLocale((*mIndexMetadata)->mCommonMetadata.locale()) {} protected: IndexOrObjectStoreId Id() const { return mIndexId; } // This should only be touched on the PBackground thread to check whether // the index has been deleted. Holding these saves a hash lookup for every // call to continue()/advance(). InitializedOnce>> mIndexMetadata; const IndexOrObjectStoreId mIndexId; const bool mUniqueIndex; const nsCString mLocale; ///< The locale if the cursor is locale-aware, otherwise empty. struct ContinueQueries { nsCString mContinueQuery; nsCString mContinueToQuery; nsCString mContinuePrimaryKeyQuery; const nsACString& GetContinueQuery(const bool hasContinueKey, const bool hasContinuePrimaryKey) const { return hasContinuePrimaryKey ? mContinuePrimaryKeyQuery : hasContinueKey ? mContinueToQuery : mContinueQuery; } }; }; class ObjectStoreCursorBase : public CursorBase { public: using CursorBase::CursorBase; static constexpr bool IsLocaleAware() { return false; } protected: IndexOrObjectStoreId Id() const { return mObjectStoreId; } struct ContinueQueries { nsCString mContinueQuery; nsCString mContinueToQuery; const nsACString& GetContinueQuery(const bool hasContinueKey, const bool hasContinuePrimaryKey) const { MOZ_ASSERT(!hasContinuePrimaryKey); return hasContinueKey ? mContinueToQuery : mContinueQuery; } }; }; using FilesArray = nsTArray>; struct PseudoFilesArray { static constexpr bool IsEmpty() { return true; } static constexpr void Clear() {} }; template using FilesArrayT = std::conditional_t::IsKeyOnlyCursor, FilesArray, PseudoFilesArray>; class ValueCursorBase { friend struct ValuePopulateResponseHelper; friend struct ValuePopulateResponseHelper; protected: explicit ValueCursorBase(TransactionBase* const aTransaction) : mDatabase(aTransaction->GetDatabasePtr()), mFileManager(mDatabase->GetFileManagerPtr()), mBackgroundParent(WrapNotNull(aTransaction->GetBackgroundParent())) { MOZ_ASSERT(mDatabase); } void ProcessFiles(CursorResponse& aResponse, const FilesArray& aFiles); ~ValueCursorBase() { MOZ_ASSERT(!mBackgroundParent); } const SafeRefPtr mDatabase; const NotNull> mFileManager; InitializedOnce> mBackgroundParent; }; class KeyCursorBase { protected: explicit KeyCursorBase(TransactionBase* const /*aTransaction*/) {} static constexpr void ProcessFiles(CursorResponse& aResponse, const PseudoFilesArray& aFiles) {} }; template class CursorOpBaseHelperBase; template class Cursor final : public std::conditional_t< CursorTypeTraits::IsObjectStoreCursor, ObjectStoreCursorBase, IndexCursorBase>, public std::conditional_t::IsKeyOnlyCursor, KeyCursorBase, ValueCursorBase> { using Base = std::conditional_t::IsObjectStoreCursor, ObjectStoreCursorBase, IndexCursorBase>; using KeyValueBase = std::conditional_t::IsKeyOnlyCursor, KeyCursorBase, ValueCursorBase>; static constexpr bool IsIndexCursor = !CursorTypeTraits::IsObjectStoreCursor; static constexpr bool IsValueCursor = !CursorTypeTraits::IsKeyOnlyCursor; class CursorOpBase; class OpenOp; class ContinueOp; using Base::Id; using CursorBase::Manager; using CursorBase::mDirection; using CursorBase::mObjectStoreId; using CursorBase::mTransaction; using typename CursorBase::ActorDestroyReason; using TypedOpenOpHelper = std::conditional_t, ObjectStoreOpenOpHelper>; friend class CursorOpBaseHelperBase; friend class CommonOpenOpHelper; friend TypedOpenOpHelper; friend class OpenOpHelper; CursorOpBase* mCurrentlyRunningOp = nullptr; LazyInitializedOnce mContinueQueries; // Only called by TransactionBase. bool Start(const OpenCursorParams& aParams) final; void SendResponseInternal(CursorResponse& aResponse, const FilesArrayT& aFiles); // Must call SendResponseInternal! bool SendResponse(const CursorResponse& aResponse) = delete; // IPDL methods. void ActorDestroy(ActorDestroyReason aWhy) override; mozilla::ipc::IPCResult RecvDeleteMe() override; mozilla::ipc::IPCResult RecvContinue( const CursorRequestParams& aParams, const Key& aCurrentKey, const Key& aCurrentObjectStoreKey) override; public: Cursor(SafeRefPtr aTransaction, SafeRefPtr aObjectStoreMetadata, SafeRefPtr aIndexMetadata, typename Base::Direction aDirection, typename Base::ConstructFromTransactionBase aConstructionTag) : Base{std::move(aTransaction), std::move(aObjectStoreMetadata), std::move(aIndexMetadata), aDirection, aConstructionTag}, KeyValueBase{this->mTransaction.unsafeGetRawPtr()} {} Cursor(SafeRefPtr aTransaction, SafeRefPtr aObjectStoreMetadata, typename Base::Direction aDirection, typename Base::ConstructFromTransactionBase aConstructionTag) : Base{std::move(aTransaction), std::move(aObjectStoreMetadata), aDirection, aConstructionTag}, KeyValueBase{this->mTransaction.unsafeGetRawPtr()} {} private: void SetOptionalKeyRange(const Maybe& aOptionalKeyRange, bool* aOpen); bool VerifyRequestParams(const CursorRequestParams& aParams, const CursorPosition& aPosition) const; ~Cursor() final = default; }; template class Cursor::CursorOpBase : public TransactionDatabaseOperationBase { friend class CursorOpBaseHelperBase; protected: RefPtr mCursor; FilesArrayT mFiles; // TODO: Consider removing this member // entirely if we are no value cursor. CursorResponse mResponse; #ifdef DEBUG bool mResponseSent; #endif protected: explicit CursorOpBase(Cursor* aCursor) : TransactionDatabaseOperationBase(aCursor->mTransaction.clonePtr()), mCursor(aCursor) #ifdef DEBUG , mResponseSent(false) #endif { AssertIsOnBackgroundThread(); MOZ_ASSERT(aCursor); } ~CursorOpBase() override = default; bool SendFailureResult(nsresult aResultCode) final; nsresult SendSuccessResult() final; void Cleanup() override; }; template class OpenOpHelper; using ResponseSizeOrError = Result; template class CursorOpBaseHelperBase { public: explicit CursorOpBaseHelperBase( typename Cursor::CursorOpBase& aOp) : mOp{aOp} {} ResponseSizeOrError PopulateResponseFromStatement(mozIStorageStatement* aStmt, bool aInitializeResponse, Key* const aOptOutSortKey); void PopulateExtraResponses(mozIStorageStatement* aStmt, uint32_t aMaxExtraCount, const size_t aInitialResponseSize, const nsACString& aOperation, Key* const aOptPreviousSortKey); protected: Cursor& GetCursor() { MOZ_ASSERT(mOp.mCursor); return *mOp.mCursor; } void SetResponse(CursorResponse aResponse) { mOp.mResponse = std::move(aResponse); } protected: typename Cursor::CursorOpBase& mOp; }; class CommonOpenOpHelperBase { protected: static void AppendConditionClause(const nsACString& aColumnName, const nsACString& aStatementParameterName, bool aLessThan, bool aEquals, nsCString& aResult); }; template class CommonOpenOpHelper : public CursorOpBaseHelperBase, protected CommonOpenOpHelperBase { public: explicit CommonOpenOpHelper(typename Cursor::OpenOp& aOp) : CursorOpBaseHelperBase{aOp} {} protected: using CursorOpBaseHelperBase::GetCursor; using CursorOpBaseHelperBase::PopulateExtraResponses; using CursorOpBaseHelperBase::PopulateResponseFromStatement; using CursorOpBaseHelperBase::SetResponse; const Maybe& GetOptionalKeyRange() const { // This downcast is safe, since we initialized mOp from an OpenOp in the // ctor. return static_cast::OpenOp&>(this->mOp) .mOptionalKeyRange; } nsresult ProcessStatementSteps(mozIStorageStatement* aStmt); }; template class ObjectStoreOpenOpHelper : protected CommonOpenOpHelper { public: using CommonOpenOpHelper::CommonOpenOpHelper; protected: using CommonOpenOpHelper::GetCursor; using CommonOpenOpHelper::GetOptionalKeyRange; using CommonOpenOpHelper::AppendConditionClause; void PrepareKeyConditionClauses(const nsACString& aDirectionClause, const nsACString& aQueryStart); }; template class IndexOpenOpHelper : protected CommonOpenOpHelper { public: using CommonOpenOpHelper::CommonOpenOpHelper; protected: using CommonOpenOpHelper::GetCursor; using CommonOpenOpHelper::GetOptionalKeyRange; using CommonOpenOpHelper::AppendConditionClause; void PrepareIndexKeyConditionClause( const nsACString& aDirectionClause, const nsLiteralCString& aObjectDataKeyPrefix, nsAutoCString aQueryStart); }; template <> class OpenOpHelper : public ObjectStoreOpenOpHelper { public: using ObjectStoreOpenOpHelper< IDBCursorType::ObjectStore>::ObjectStoreOpenOpHelper; nsresult DoDatabaseWork(DatabaseConnection* aConnection); }; template <> class OpenOpHelper : public ObjectStoreOpenOpHelper { public: using ObjectStoreOpenOpHelper< IDBCursorType::ObjectStoreKey>::ObjectStoreOpenOpHelper; nsresult DoDatabaseWork(DatabaseConnection* aConnection); }; template <> class OpenOpHelper : IndexOpenOpHelper { private: void PrepareKeyConditionClauses(const nsACString& aDirectionClause, nsAutoCString aQueryStart) { PrepareIndexKeyConditionClause(aDirectionClause, "index_table."_ns, std::move(aQueryStart)); } public: using IndexOpenOpHelper::IndexOpenOpHelper; nsresult DoDatabaseWork(DatabaseConnection* aConnection); }; template <> class OpenOpHelper : IndexOpenOpHelper { private: void PrepareKeyConditionClauses(const nsACString& aDirectionClause, nsAutoCString aQueryStart) { PrepareIndexKeyConditionClause(aDirectionClause, ""_ns, std::move(aQueryStart)); } public: using IndexOpenOpHelper::IndexOpenOpHelper; nsresult DoDatabaseWork(DatabaseConnection* aConnection); }; template class Cursor::OpenOp final : public CursorOpBase { friend class Cursor; friend class CommonOpenOpHelper; const Maybe mOptionalKeyRange; using CursorOpBase::mCursor; using CursorOpBase::mResponse; // Only created by Cursor. OpenOp(Cursor* const aCursor, const Maybe& aOptionalKeyRange) : CursorOpBase(aCursor), mOptionalKeyRange(aOptionalKeyRange) {} // Reference counted. ~OpenOp() override = default; nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; }; template class Cursor::ContinueOp final : public Cursor::CursorOpBase { friend class Cursor; using CursorOpBase::mCursor; using CursorOpBase::mResponse; const CursorRequestParams mParams; // Only created by Cursor. ContinueOp(Cursor* const aCursor, CursorRequestParams aParams, CursorPosition aPosition) : CursorOpBase(aCursor), mParams(std::move(aParams)), mCurrentPosition{std::move(aPosition)} { MOZ_ASSERT(mParams.type() != CursorRequestParams::T__None); } // Reference counted. ~ContinueOp() override = default; nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; const CursorPosition mCurrentPosition; }; class Utils final : public PBackgroundIndexedDBUtilsParent { #ifdef DEBUG bool mActorDestroyed; #endif public: Utils(); NS_INLINE_DECL_THREADSAFE_REFCOUNTING(mozilla::dom::indexedDB::Utils) private: // Reference counted. ~Utils() override; // IPDL methods are only called by IPDL. void ActorDestroy(ActorDestroyReason aWhy) override; mozilla::ipc::IPCResult RecvDeleteMe() override; mozilla::ipc::IPCResult RecvGetFileReferences( const PersistenceType& aPersistenceType, const nsACString& aOrigin, const nsAString& aDatabaseName, const int64_t& aFileId, int32_t* aRefCnt, int32_t* aDBRefCnt, bool* aResult) override; }; /******************************************************************************* * Other class declarations ******************************************************************************/ struct DatabaseActorInfo final { friend class mozilla::DefaultDelete; SafeRefPtr mMetadata; nsTArray>> mLiveDatabases; RefPtr mWaitingFactoryOp; DatabaseActorInfo(SafeRefPtr aMetadata, NotNull aDatabase) : mMetadata(std::move(aMetadata)) { MOZ_COUNT_CTOR(DatabaseActorInfo); mLiveDatabases.AppendElement(aDatabase); } private: ~DatabaseActorInfo() { MOZ_ASSERT(mLiveDatabases.IsEmpty()); MOZ_ASSERT(!mWaitingFactoryOp || !mWaitingFactoryOp->HasBlockedDatabases()); MOZ_COUNT_DTOR(DatabaseActorInfo); } }; class DatabaseLoggingInfo final { #ifdef DEBUG // Just for potential warnings. friend class Factory; #endif LoggingInfo mLoggingInfo; public: explicit DatabaseLoggingInfo(const LoggingInfo& aLoggingInfo) : mLoggingInfo(aLoggingInfo) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aLoggingInfo.nextTransactionSerialNumber()); MOZ_ASSERT(aLoggingInfo.nextVersionChangeTransactionSerialNumber()); MOZ_ASSERT(aLoggingInfo.nextRequestSerialNumber()); } const nsID& Id() const { AssertIsOnBackgroundThread(); return mLoggingInfo.backgroundChildLoggingId(); } int64_t NextTransactionSN(IDBTransaction::Mode aMode) { AssertIsOnBackgroundThread(); MOZ_ASSERT(mLoggingInfo.nextTransactionSerialNumber() < INT64_MAX); MOZ_ASSERT(mLoggingInfo.nextVersionChangeTransactionSerialNumber() > INT64_MIN); if (aMode == IDBTransaction::Mode::VersionChange) { return mLoggingInfo.nextVersionChangeTransactionSerialNumber()--; } return mLoggingInfo.nextTransactionSerialNumber()++; } uint64_t NextRequestSN() { AssertIsOnBackgroundThread(); MOZ_ASSERT(mLoggingInfo.nextRequestSerialNumber() < UINT64_MAX); return mLoggingInfo.nextRequestSerialNumber()++; } NS_INLINE_DECL_REFCOUNTING(DatabaseLoggingInfo) private: ~DatabaseLoggingInfo(); }; class QuotaClient final : public mozilla::dom::quota::Client { static QuotaClient* sInstance; nsCOMPtr mBackgroundThread; nsCOMPtr mDeleteTimer; nsTArray> mMaintenanceQueue; RefPtr mCurrentMaintenance; RefPtr mMaintenanceThreadPool; nsClassHashtable, nsTArray> mPendingDeleteInfos; public: QuotaClient(); static QuotaClient* GetInstance() { AssertIsOnBackgroundThread(); return sInstance; } nsIEventTarget* BackgroundThread() const { MOZ_ASSERT(mBackgroundThread); return mBackgroundThread; } nsresult AsyncDeleteFile(DatabaseFileManager* aFileManager, int64_t aFileId); nsresult FlushPendingFileDeletions(); RefPtr GetCurrentMaintenance() const { return mCurrentMaintenance; } void NoteFinishedMaintenance(Maintenance* aMaintenance) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aMaintenance); MOZ_ASSERT(mCurrentMaintenance == aMaintenance); mCurrentMaintenance = nullptr; QuotaManager::MaybeRecordQuotaClientShutdownStep(quota::Client::IDB, "Maintenance finished"_ns); ProcessMaintenanceQueue(); } nsThreadPool* GetOrCreateThreadPool(); NS_INLINE_DECL_THREADSAFE_REFCOUNTING(mozilla::dom::indexedDB::QuotaClient, override) mozilla::dom::quota::Client::Type GetType() override; nsresult UpgradeStorageFrom1_0To2_0(nsIFile* aDirectory) override; nsresult UpgradeStorageFrom2_1To2_2(nsIFile* aDirectory) override; Result InitOrigin(PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, const AtomicBool& aCanceled) override; nsresult InitOriginWithoutTracking(PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, const AtomicBool& aCanceled) override; Result GetUsageForOrigin( PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, const AtomicBool& aCanceled) override; void OnOriginClearCompleted(PersistenceType aPersistenceType, const nsACString& aOrigin) override; void OnRepositoryClearCompleted(PersistenceType aPersistenceType) override; void ReleaseIOThreadObjects() override; void AbortOperationsForLocks( const DirectoryLockIdTable& aDirectoryLockIds) override; void AbortOperationsForProcess(ContentParentId aContentParentId) override; void AbortAllOperations() override; void StartIdleMaintenance() override; void StopIdleMaintenance() override; private: ~QuotaClient() override; void InitiateShutdown() override; bool IsShutdownCompleted() const override; nsCString GetShutdownStatus() const override; void ForceKillActors() override; void FinalizeShutdown() override; static void DeleteTimerCallback(nsITimer* aTimer, void* aClosure); void AbortAllMaintenances(); Result, nsresult> GetDirectory( const OriginMetadata& aOriginMetadata); struct SubdirectoriesToProcessAndDatabaseFilenames { AutoTArray subdirsToProcess; nsTHashSet databaseFilenames{20}; }; struct SubdirectoriesToProcessAndDatabaseFilenamesAndObsoleteFilenames { AutoTArray subdirsToProcess; nsTHashSet databaseFilenames{20}; nsTHashSet obsoleteFilenames{20}; }; enum class ObsoleteFilenamesHandling { Include, Omit }; template using GetDatabaseFilenamesResult = std::conditional_t< ObsoleteFilenames == ObsoleteFilenamesHandling::Include, SubdirectoriesToProcessAndDatabaseFilenamesAndObsoleteFilenames, SubdirectoriesToProcessAndDatabaseFilenames>; // Returns a two-part or three-part structure: // // The first part is an array of subdirectories to process. // // The second part is a hashtable of database filenames. // // When ObsoleteFilenames is ObsoleteFilenamesHandling::Include, will also // collect files based on the marker files. For now, // GetUsageForOriginInternal() is the only consumer of this result because it // checks those unfinished deletion and clean them up after that. template Result, nsresult> GetDatabaseFilenames(nsIFile& aDirectory, const AtomicBool& aCanceled); nsresult GetUsageForOriginInternal(PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, const AtomicBool& aCanceled, bool aInitializing, UsageInfo* aUsageInfo); // Runs on the PBackground thread. Checks to see if there's a queued // Maintenance to run. void ProcessMaintenanceQueue(); }; class DeleteFilesRunnable final : public Runnable { using DirectoryLock = mozilla::dom::quota::DirectoryLock; enum State { // Just created on the PBackground thread. Next step is // State_DirectoryOpenPending. State_Initial, // Waiting for directory open allowed on the main thread. The next step is // State_DatabaseWorkOpen. State_DirectoryOpenPending, // Waiting to do/doing work on the QuotaManager IO thread. The next step is // State_UnblockingOpen. State_DatabaseWorkOpen, // Notifying the QuotaManager that it can proceed to the next operation on // the main thread. Next step is State_Completed. State_UnblockingOpen, // All done. State_Completed }; nsCOMPtr mOwningEventTarget; SafeRefPtr mFileManager; RefPtr mDirectoryLock; nsTArray mFileIds; State mState; public: DeleteFilesRunnable(SafeRefPtr aFileManager, nsTArray&& aFileIds); void RunImmediately(); private: ~DeleteFilesRunnable() = default; void Open(); void DoDatabaseWork(); void Finish(); void UnblockOpen(); NS_DECL_NSIRUNNABLE void DirectoryLockAcquired(DirectoryLock* aLock); void DirectoryLockFailed(); }; class Maintenance final : public Runnable { struct DirectoryInfo final { InitializedOnce mOriginMetadata; InitializedOnce> mDatabasePaths; const PersistenceType mPersistenceType; DirectoryInfo(PersistenceType aPersistenceType, OriginMetadata aOriginMetadata, nsTArray&& aDatabasePaths); DirectoryInfo(const DirectoryInfo& aOther) = delete; DirectoryInfo(DirectoryInfo&& aOther) = delete; ~DirectoryInfo() { MOZ_COUNT_DTOR(Maintenance::DirectoryInfo); } }; enum class State { // Newly created on the PBackground thread. Will proceed immediately or be // added to the maintenance queue. The next step is either // DirectoryOpenPending if IndexedDatabaseManager is running, or // CreateIndexedDatabaseManager if not. Initial = 0, // Create IndexedDatabaseManager on the main thread. The next step is either // Finishing if IndexedDatabaseManager initialization fails, or // IndexedDatabaseManagerOpen if initialization succeeds. CreateIndexedDatabaseManager, // Call OpenDirectory() on the PBackground thread. The next step is // DirectoryOpenPending. IndexedDatabaseManagerOpen, // Waiting for directory open allowed on the PBackground thread. The next // step is either Finishing if directory lock failed to acquire, or // DirectoryWorkOpen if directory lock is acquired. DirectoryOpenPending, // Waiting to do/doing work on the QuotaManager IO thread. The next step is // BeginDatabaseMaintenance. DirectoryWorkOpen, // Dispatching a runnable for each database on the PBackground thread. The // next state is either WaitingForDatabaseMaintenancesToComplete if at least // one runnable has been dispatched, or Finishing otherwise. BeginDatabaseMaintenance, // Waiting for DatabaseMaintenance to finish on maintenance thread pool. // The next state is Finishing if the last runnable has finished. WaitingForDatabaseMaintenancesToComplete, // Waiting to finish/finishing on the PBackground thread. The next step is // Completed. Finishing, // All done. Complete }; RefPtr mQuotaClient; PRTime mStartTime; RefPtr mPendingDirectoryLock; RefPtr mDirectoryLock; nsTArray mDirectoryInfos; nsTHashMap mDatabaseMaintenances; nsresult mResultCode; Atomic mAborted; State mState; public: explicit Maintenance(QuotaClient* aQuotaClient) : Runnable("dom::indexedDB::Maintenance"), mQuotaClient(aQuotaClient), mStartTime(PR_Now()), mResultCode(NS_OK), mAborted(false), mState(State::Initial) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aQuotaClient); MOZ_ASSERT(QuotaClient::GetInstance() == aQuotaClient); MOZ_ASSERT(mStartTime); } nsIEventTarget* BackgroundThread() const { MOZ_ASSERT(mQuotaClient); return mQuotaClient->BackgroundThread(); } PRTime StartTime() const { return mStartTime; } bool IsAborted() const { return mAborted; } void RunImmediately() { MOZ_ASSERT(mState == State::Initial); Unused << this->Run(); } void Abort(); void RegisterDatabaseMaintenance(DatabaseMaintenance* aDatabaseMaintenance); void UnregisterDatabaseMaintenance(DatabaseMaintenance* aDatabaseMaintenance); RefPtr GetDatabaseMaintenance( const nsAString& aDatabasePath) const { AssertIsOnBackgroundThread(); return mDatabaseMaintenances.Get(aDatabasePath); } void Stringify(nsACString& aResult) const; private: ~Maintenance() override { MOZ_ASSERT(mState == State::Complete); MOZ_ASSERT(!mDatabaseMaintenances.Count()); } // Runs on the PBackground thread. Checks if IndexedDatabaseManager is // running. Calls OpenDirectory() or dispatches to the main thread on which // CreateIndexedDatabaseManager() is called. nsresult Start(); // Runs on the main thread. Once IndexedDatabaseManager is created it will // dispatch to the PBackground thread on which OpenDirectory() is called. nsresult CreateIndexedDatabaseManager(); // Runs on the PBackground thread. Once QuotaManager has given a lock it will // call DirectoryOpen(). nsresult OpenDirectory(); // Runs on the PBackground thread. Dispatches to the QuotaManager I/O thread. nsresult DirectoryOpen(); // Runs on the QuotaManager I/O thread. Once it finds databases it will // dispatch to the PBackground thread on which BeginDatabaseMaintenance() // is called. nsresult DirectoryWork(); // Runs on the PBackground thread. It dispatches a runnable for each database. nsresult BeginDatabaseMaintenance(); // Runs on the PBackground thread. Called when the maintenance is finished or // if any of above methods fails. void Finish(); NS_DECL_NSIRUNNABLE void DirectoryLockAcquired(DirectoryLock* aLock); void DirectoryLockFailed(); }; Maintenance::DirectoryInfo::DirectoryInfo(PersistenceType aPersistenceType, OriginMetadata aOriginMetadata, nsTArray&& aDatabasePaths) : mOriginMetadata(std::move(aOriginMetadata)), mDatabasePaths(std::move(aDatabasePaths)), mPersistenceType(aPersistenceType) { MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_INVALID); MOZ_ASSERT(!mOriginMetadata->mGroup.IsEmpty()); MOZ_ASSERT(!mOriginMetadata->mOrigin.IsEmpty()); #ifdef DEBUG MOZ_ASSERT(!mDatabasePaths->IsEmpty()); for (const nsAString& databasePath : *mDatabasePaths) { MOZ_ASSERT(!databasePath.IsEmpty()); } #endif MOZ_COUNT_CTOR(Maintenance::DirectoryInfo); } class DatabaseMaintenance final : public Runnable { // The minimum amount of time that has passed since the last vacuum before we // will attempt to analyze the database for fragmentation. static const PRTime kMinVacuumAge = PRTime(PR_USEC_PER_SEC) * 60 * 60 * 24 * 7; // If the percent of database pages that are not in contiguous order is higher // than this percentage we will attempt a vacuum. static const int32_t kPercentUnorderedThreshold = 30; // If the percent of file size growth since the last vacuum is higher than // this percentage we will attempt a vacuum. static const int32_t kPercentFileSizeGrowthThreshold = 10; // The number of freelist pages beyond which we will favor an incremental // vacuum over a full vacuum. static const int32_t kMaxFreelistThreshold = 5; // If the percent of unused file bytes in the database exceeds this percentage // then we will attempt a full vacuum. static const int32_t kPercentUnusedThreshold = 20; enum class MaintenanceAction { Nothing = 0, IncrementalVacuum, FullVacuum }; RefPtr mMaintenance; RefPtr mDirectoryLock; const OriginMetadata mOriginMetadata; const nsString mDatabasePath; int64_t mDirectoryLockId; nsCOMPtr mCompleteCallback; const PersistenceType mPersistenceType; const Maybe mMaybeKey; Atomic mAborted; DataMutex> mSharedStorageConnection; public: DatabaseMaintenance(Maintenance* aMaintenance, DirectoryLock* aDirectoryLock, PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, const nsAString& aDatabasePath, const Maybe& aMaybeKey) : Runnable("dom::indexedDB::DatabaseMaintenance"), mMaintenance(aMaintenance), mDirectoryLock(aDirectoryLock), mOriginMetadata(aOriginMetadata), mDatabasePath(aDatabasePath), mPersistenceType(aPersistenceType), mMaybeKey{aMaybeKey}, mAborted(false), mSharedStorageConnection("sharedStorageConnection") { MOZ_ASSERT(aDirectoryLock); MOZ_ASSERT(mDirectoryLock->Id() >= 0); mDirectoryLockId = mDirectoryLock->Id(); } const nsAString& DatabasePath() const { return mDatabasePath; } void WaitForCompletion(nsIRunnable* aCallback) { AssertIsOnBackgroundThread(); MOZ_ASSERT(!mCompleteCallback); mCompleteCallback = aCallback; } void Stringify(nsACString& aResult) const; nsresult Abort(); private: ~DatabaseMaintenance() override = default; // Runs on maintenance thread pool. Does maintenance on the database. void PerformMaintenanceOnDatabase(); // Runs on maintenance thread pool as part of PerformMaintenanceOnDatabase. nsresult CheckIntegrity(mozIStorageConnection& aConnection, bool* aOk); // Runs on maintenance thread pool as part of PerformMaintenanceOnDatabase. nsresult DetermineMaintenanceAction(mozIStorageConnection& aConnection, nsIFile* aDatabaseFile, MaintenanceAction* aMaintenanceAction); // Runs on maintenance thread pool as part of PerformMaintenanceOnDatabase. void IncrementalVacuum(mozIStorageConnection& aConnection); // Runs on maintenance thread pool as part of PerformMaintenanceOnDatabase. void FullVacuum(mozIStorageConnection& aConnection, nsIFile* aDatabaseFile); // Runs on the PBackground thread. It dispatches a complete callback and // unregisters from Maintenance. void RunOnOwningThread(); // Runs on maintenance thread pool. Once it performs database maintenance // it will dispatch to the PBackground thread on which RunOnOwningThread() // is called. void RunOnConnectionThread(); // TODO: Could QuotaClient::IsShuttingDownOnNonBackgroundThread() call // be part of mMaintenance::IsAborted() ? inline bool IsAborted() const { return mMaintenance->IsAborted() || mAborted || NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()); } NS_DECL_NSIRUNNABLE }; #ifdef DEBUG class DEBUGThreadSlower final : public nsIThreadObserver { public: DEBUGThreadSlower() { AssertIsOnBackgroundThread(); MOZ_ASSERT(kDEBUGThreadSleepMS); } NS_DECL_ISUPPORTS private: ~DEBUGThreadSlower() { AssertIsOnBackgroundThread(); } NS_DECL_NSITHREADOBSERVER }; #endif // DEBUG /******************************************************************************* * Helper classes ******************************************************************************/ // XXX Get rid of FileHelper and move the functions into DatabaseFileManager. // Then, DatabaseFileManager::Get(Journal)Directory and // DatabaseFileManager::GetFileForId might eventually be made private. class MOZ_STACK_CLASS FileHelper final { const SafeRefPtr mFileManager; LazyInitializedOnce>> mFileDirectory; LazyInitializedOnce>> mJournalDirectory; class ReadCallback; LazyInitializedOnce>> mReadCallback; public: explicit FileHelper(SafeRefPtr&& aFileManager) : mFileManager(std::move(aFileManager)) { MOZ_ASSERT(mFileManager); } nsresult Init(); [[nodiscard]] nsCOMPtr GetFile(const DatabaseFileInfo& aFileInfo); [[nodiscard]] nsCOMPtr GetJournalFile( const DatabaseFileInfo& aFileInfo); nsresult CreateFileFromStream(nsIFile& aFile, nsIFile& aJournalFile, nsIInputStream& aInputStream, bool aCompress, const Maybe& aMaybeKey); private: nsresult SyncCopy(nsIInputStream& aInputStream, nsIOutputStream& aOutputStream, char* aBuffer, uint32_t aBufferSize); nsresult SyncRead(nsIInputStream& aInputStream, char* aBuffer, uint32_t aBufferSize, uint32_t* aRead); }; /******************************************************************************* * Helper Functions ******************************************************************************/ bool GetFilenameBase(const nsAString& aFilename, const nsAString& aSuffix, nsDependentSubstring& aFilenameBase) { MOZ_ASSERT(!aFilename.IsEmpty()); MOZ_ASSERT(aFilenameBase.IsEmpty()); if (!StringEndsWith(aFilename, aSuffix) || aFilename.Length() == aSuffix.Length()) { return false; } MOZ_ASSERT(aFilename.Length() > aSuffix.Length()); aFilenameBase.Rebind(aFilename, 0, aFilename.Length() - aSuffix.Length()); return true; } class EncryptedFileBlobImpl final : public FileBlobImpl { public: EncryptedFileBlobImpl(const nsCOMPtr& aNativeFile, const DatabaseFileInfo::IdType aId, const CipherKey& aKey) : FileBlobImpl{aNativeFile}, mKey{aKey} { SetFileId(aId); } uint64_t GetSize(ErrorResult& aRv) override { nsCOMPtr inputStream; CreateInputStream(getter_AddRefs(inputStream), aRv); if (aRv.Failed()) { return 0; } MOZ_ASSERT(inputStream); QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(inputStream, Available), 0, [&aRv](const nsresult rv) { aRv = rv; }); } void CreateInputStream(nsIInputStream** aInputStream, ErrorResult& aRv) const override { nsCOMPtr baseInputStream; FileBlobImpl::CreateInputStream(getter_AddRefs(baseInputStream), aRv); if (NS_WARN_IF(aRv.Failed())) { return; } *aInputStream = MakeAndAddRef>( WrapNotNull(std::move(baseInputStream)), kEncryptedStreamBlockSize, mKey) .take(); } void GetBlobImplType(nsAString& aBlobImplType) const override { aBlobImplType = u"EncryptedFileBlobImpl"_ns; } already_AddRefed CreateSlice(uint64_t aStart, uint64_t aLength, const nsAString& aContentType, ErrorResult& aRv) const override { MOZ_CRASH("Not implemented because this should be unreachable."); } private: const CipherKey mKey; }; RefPtr CreateFileBlobImpl(const Database& aDatabase, const nsCOMPtr& aNativeFile, const DatabaseFileInfo::IdType aId) { if (aDatabase.IsInPrivateBrowsing()) { nsCString keyId; keyId.AppendInt(aId); const auto& key = aDatabase.GetFileManager().MutableCipherKeyManagerRef().Get(keyId); MOZ_RELEASE_ASSERT(key.isSome()); return MakeRefPtr(aNativeFile, aId, *key); } auto impl = MakeRefPtr(aNativeFile); impl->SetFileId(aId); return impl; } Result, nsresult> SerializeStructuredCloneFiles(const SafeRefPtr& aDatabase, const nsTArray& aFiles, bool aForPreprocess) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aDatabase); if (aFiles.IsEmpty()) { return nsTArray{}; } const nsCOMPtr directory = aDatabase->GetFileManager().GetCheckedDirectory(); QM_TRY(OkIf(directory), Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR), IDB_REPORT_INTERNAL_ERR_LAMBDA); nsTArray serializedStructuredCloneFiles; QM_TRY(OkIf(serializedStructuredCloneFiles.SetCapacity(aFiles.Length(), fallible)), Err(NS_ERROR_OUT_OF_MEMORY)); QM_TRY(TransformIfAbortOnErr( aFiles, MakeBackInserter(serializedStructuredCloneFiles), [aForPreprocess](const auto& file) { return !aForPreprocess || file.Type() == StructuredCloneFileBase::eStructuredClone; }, [&directory, &aDatabase, aForPreprocess]( const auto& file) -> Result { const int64_t fileId = file.FileInfo().Id(); MOZ_ASSERT(fileId > 0); const nsCOMPtr nativeFile = mozilla::dom::indexedDB::DatabaseFileManager::GetCheckedFileForId( directory, fileId); QM_TRY(OkIf(nativeFile), Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR), IDB_REPORT_INTERNAL_ERR_LAMBDA); switch (file.Type()) { case StructuredCloneFileBase::eStructuredClone: if (!aForPreprocess) { return SerializedStructuredCloneFile{ null_t(), StructuredCloneFileBase::eStructuredClone}; } [[fallthrough]]; case StructuredCloneFileBase::eBlob: { const auto impl = CreateFileBlobImpl(*aDatabase, nativeFile, file.FileInfo().Id()); IPCBlob ipcBlob; // This can only fail if the child has crashed. QM_TRY(MOZ_TO_RESULT(IPCBlobUtils::Serialize(impl, ipcBlob)), Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR), IDB_REPORT_INTERNAL_ERR_LAMBDA); aDatabase->MapBlob(ipcBlob, file.FileInfoPtr()); return SerializedStructuredCloneFile{ipcBlob, file.Type()}; } case StructuredCloneFileBase::eMutableFile: case StructuredCloneFileBase::eWasmBytecode: case StructuredCloneFileBase::eWasmCompiled: { // Set file() to null, support for storing WebAssembly.Modules has // been removed in bug 1469395. Support for de-serialization of // WebAssembly.Modules modules has been removed in bug 1561876. // Support for MutableFile has been removed in bug 1500343. Full // removal is tracked in bug 1487479. return SerializedStructuredCloneFile{null_t(), file.Type()}; } default: MOZ_CRASH("Should never get here!"); } })); return std::move(serializedStructuredCloneFiles); } bool IsFileNotFoundError(const nsresult aRv) { return aRv == NS_ERROR_FILE_NOT_FOUND; } enum struct Idempotency { Yes, No }; // Delete a file, decreasing the quota usage as appropriate. If the file no // longer exists but aIdempotency is Idempotency::Yes, success is returned, // although quota usage can't be decreased. (With the assumption being that the // file was already deleted prior to this logic running, and the non-existent // file was no longer tracked by quota because it didn't exist at // initialization time or a previous deletion call updated the usage.) nsresult DeleteFile(nsIFile& aFile, QuotaManager* const aQuotaManager, const PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, const Idempotency aIdempotency) { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!IsOnBackgroundThread()); // Callers which pass Idempotency::Yes call this function without checking if // the file already exists (idempotent usage). QM_OR_ELSE_WARN_IF is not used // here since we just want to log NS_ERROR_FILE_NOT_FOUND results and not spam // the reports. // Theoretically, there should be no QM_OR_ELSE_(WARN|LOG_VERBOSE)_IF when a // caller passes Idempotency::No, but it's simpler when the predicate just // always returns false in that case. const auto isIgnorableError = [&aIdempotency]() -> bool (*)(nsresult) { if (aIdempotency == Idempotency::Yes) { return IsFileNotFoundError; } return [](const nsresult rv) { return false; }; }(); QM_TRY_INSPECT( const auto& fileSize, ([aQuotaManager, &aFile, isIgnorableError]() -> Result, nsresult> { if (aQuotaManager) { QM_TRY_INSPECT( const Maybe& fileSize, QM_OR_ELSE_LOG_VERBOSE_IF( // Expression. MOZ_TO_RESULT_INVOKE_MEMBER(aFile, GetFileSize) .map([](const int64_t val) { return Some(val); }), // Predicate. isIgnorableError, // Fallback. ErrToDefaultOk>)); // XXX Can we really assert that the file size is not 0 if // it existed? This might be violated by external // influences. MOZ_ASSERT(!fileSize || fileSize.value() >= 0); return fileSize; } return Some(int64_t(0)); }())); if (!fileSize) { return NS_OK; } QM_TRY_INSPECT(const auto& didExist, QM_OR_ELSE_LOG_VERBOSE_IF( // Expression. MOZ_TO_RESULT(aFile.Remove(false)).map(Some), // Predicate. isIgnorableError, // Fallback. ErrToDefaultOk>)); if (!didExist) { // XXX If we get here, this means that the file still existed when we // queried its size, but no longer when we tried to remove it. Not sure if // this should really be silently accepted in idempotent mode. return NS_OK; } if (fileSize.value() > 0) { MOZ_ASSERT(aQuotaManager); aQuotaManager->DecreaseUsageForClient( ClientMetadata{aOriginMetadata, Client::IDB}, fileSize.value()); } return NS_OK; } nsresult DeleteFile(nsIFile& aDirectory, const nsAString& aFilename, QuotaManager* const aQuotaManager, const PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, const Idempotency aIdempotent) { AssertIsOnIOThread(); MOZ_ASSERT(!aFilename.IsEmpty()); QM_TRY_INSPECT(const auto& file, CloneFileAndAppend(aDirectory, aFilename)); return DeleteFile(*file, aQuotaManager, aPersistenceType, aOriginMetadata, aIdempotent); } // Delete files in a directory that you think exists. If the directory doesn't // exist, an error will not be returned, but warning telemetry will be // generated! So only call this on directories that you know exist (idempotent // usage, but it's not recommended). nsresult DeleteFilesNoQuota(nsIFile& aFile) { AssertIsOnIOThread(); QM_TRY_INSPECT(const auto& didExist, QM_OR_ELSE_WARN_IF( // Expression. MOZ_TO_RESULT(aFile.Remove(true)).map(Some), // Predicate. IsFileNotFoundError, // Fallback. ErrToDefaultOk>)); Unused << didExist; return NS_OK; } nsresult DeleteFilesNoQuota(nsIFile* aDirectory, const nsAString& aFilename) { AssertIsOnIOThread(); MOZ_ASSERT(aDirectory); MOZ_ASSERT(!aFilename.IsEmpty()); // The current using function hasn't initialized the origin, so in here we // don't update the size of origin. Adding this assertion for preventing from // misusing. DebugOnly quotaManager = QuotaManager::Get(); MOZ_ASSERT(!quotaManager->IsTemporaryStorageInitializedInternal()); QM_TRY_INSPECT(const auto& file, CloneFileAndAppend(*aDirectory, aFilename)); QM_TRY(MOZ_TO_RESULT(DeleteFilesNoQuota(*file))); return NS_OK; } // CreateMarkerFile and RemoveMarkerFile are a pair of functions to indicate // whether having removed all the files successfully. The marker file should // be checked before executing the next operation or initialization. Result, nsresult> CreateMarkerFile( nsIFile& aBaseDirectory, const nsAString& aDatabaseNameBase) { AssertIsOnIOThread(); MOZ_ASSERT(!aDatabaseNameBase.IsEmpty()); QM_TRY_INSPECT( const auto& markerFile, CloneFileAndAppend(aBaseDirectory, kIdbDeletionMarkerFilePrefix + aDatabaseNameBase)); // Callers call this function without checking if the file already exists // (idempotent usage). QM_OR_ELSE_WARN_IF is not used here since we just want // to log NS_ERROR_FILE_ALREADY_EXISTS result and not spam the reports. // // TODO: In theory if this file exists, then RemoveDatabaseFilesAndDirectory // should have cleaned it up, but obviously we can crash and not clean it up, // which is the whole point of the marker file. In that case, we'll realize // the marker file exists in OpenDatabaseOp::DoDatabaseWork or // GetUsageForOriginInternal and resume the removal by calling // RemoveDatabaseFilesAndDirectory again, but we will also try to create the // marker file again, so if we see this marker file, it is part // of our standard operating procedure to redundantly try and create the // marker here. We currently treat this as idempotent usage, but we could // add an additional argument to RemoveDatabaseFilesAndDirectory which would // indicate that we are resuming an unfinished removal, so the marker already // exists and doesn't have to be created, and change // QM_OR_ELSE_LOG_VERBOSE_IF to QM_OR_ELSE_WARN_IF in the end. QM_TRY(QM_OR_ELSE_LOG_VERBOSE_IF( // Expression. MOZ_TO_RESULT(markerFile->Create(nsIFile::NORMAL_FILE_TYPE, 0644)), // Predicate. IsSpecificError, // Fallback. ErrToDefaultOk<>)); return markerFile; } nsresult RemoveMarkerFile(nsIFile* aMarkerFile) { AssertIsOnIOThread(); MOZ_ASSERT(aMarkerFile); DebugOnly exists; MOZ_ASSERT(NS_SUCCEEDED(aMarkerFile->Exists(&exists))); MOZ_ASSERT(exists); QM_TRY(MOZ_TO_RESULT(aMarkerFile->Remove(false))); return NS_OK; } Result DeleteFileManagerDirectory( nsIFile& aFileManagerDirectory, QuotaManager* aQuotaManager, const PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata) { // XXX In theory, deleting can continue for other files in case of a failure, // leaving only those files behind that cause the problem actually. However, // the current architecture doesn't allow having more databases (for the same // name) on disk, so trying to delete as much as possible won't help much // because we need to delete entire .files directory in the end anyway. QM_TRY(DatabaseFileManager::TraverseFiles( aFileManagerDirectory, // KnownDirEntryOp [&aQuotaManager, aPersistenceType, &aOriginMetadata]( nsIFile& file, const bool isDirectory) -> Result { if (isDirectory) { // The journal directory doesn't count towards quota. QM_TRY_RETURN(MOZ_TO_RESULT(DeleteFilesNoQuota(file))); } // Stored files do count towards quota. QM_TRY_RETURN( MOZ_TO_RESULT(DeleteFile(file, aQuotaManager, aPersistenceType, aOriginMetadata, Idempotency::Yes))); }, // UnknownDirEntryOp [aPersistenceType, &aOriginMetadata]( nsIFile& file, const bool isDirectory) -> Result { // Unknown files and directories don't count towards quota. if (isDirectory) { QM_TRY_RETURN(MOZ_TO_RESULT(DeleteFilesNoQuota(file))); } QM_TRY_RETURN(MOZ_TO_RESULT( DeleteFile(file, /* doesn't count */ nullptr, aPersistenceType, aOriginMetadata, Idempotency::Yes))); })); QM_TRY_RETURN(MOZ_TO_RESULT(aFileManagerDirectory.Remove(false))); } // Idempotently delete all the parts of an IndexedDB database including its // SQLite database file, its WAL journal, it's shared-memory file, and its // Blob/Files sub-directory. A marker file is created prior to performing the // deletion so that in the event we crash or fail to successfully delete the // database and its files, we will re-attempt the deletion the next time the // origin is initialized using this method. Because this means the method may be // called on a partially deleted database, this method uses DeleteFile which // succeeds when the file we ask it to delete does not actually exist. The // marker file is removed once deletion has successfully completed. nsresult RemoveDatabaseFilesAndDirectory(nsIFile& aBaseDirectory, const nsAString& aDatabaseFilenameBase, QuotaManager* aQuotaManager, const PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, const nsAString& aDatabaseName) { AssertIsOnIOThread(); MOZ_ASSERT(!aDatabaseFilenameBase.IsEmpty()); AUTO_PROFILER_LABEL("RemoveDatabaseFilesAndDirectory", DOM); QM_TRY_UNWRAP(auto markerFile, CreateMarkerFile(aBaseDirectory, aDatabaseFilenameBase)); // The database file counts towards quota. QM_TRY(MOZ_TO_RESULT(DeleteFile( aBaseDirectory, aDatabaseFilenameBase + kSQLiteSuffix, aQuotaManager, aPersistenceType, aOriginMetadata, Idempotency::Yes))); // .sqlite-journal files don't count towards quota. QM_TRY(MOZ_TO_RESULT(DeleteFile(aBaseDirectory, aDatabaseFilenameBase + kSQLiteJournalSuffix, /* doesn't count */ nullptr, aPersistenceType, aOriginMetadata, Idempotency::Yes))); // .sqlite-shm files don't count towards quota. QM_TRY(MOZ_TO_RESULT(DeleteFile(aBaseDirectory, aDatabaseFilenameBase + kSQLiteSHMSuffix, /* doesn't count */ nullptr, aPersistenceType, aOriginMetadata, Idempotency::Yes))); // .sqlite-wal files do count towards quota. QM_TRY(MOZ_TO_RESULT(DeleteFile( aBaseDirectory, aDatabaseFilenameBase + kSQLiteWALSuffix, aQuotaManager, aPersistenceType, aOriginMetadata, Idempotency::Yes))); // The files directory counts towards quota. QM_TRY_INSPECT( const auto& fmDirectory, CloneFileAndAppend(aBaseDirectory, aDatabaseFilenameBase + kFileManagerDirectoryNameSuffix)); QM_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE_MEMBER(fmDirectory, Exists)); if (exists) { QM_TRY_INSPECT(const bool& isDirectory, MOZ_TO_RESULT_INVOKE_MEMBER(fmDirectory, IsDirectory)); QM_TRY(OkIf(isDirectory), NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); QM_TRY(DeleteFileManagerDirectory(*fmDirectory, aQuotaManager, aPersistenceType, aOriginMetadata)); } IndexedDatabaseManager* mgr = IndexedDatabaseManager::Get(); MOZ_ASSERT_IF(aQuotaManager, mgr); if (mgr) { mgr->InvalidateFileManager(aPersistenceType, aOriginMetadata.mOrigin, aDatabaseName); } QM_TRY(MOZ_TO_RESULT(RemoveMarkerFile(markerFile))); return NS_OK; } /******************************************************************************* * Globals ******************************************************************************/ // Counts the number of "live" Factory, FactoryOp and Database instances. uint64_t gBusyCount = 0; using FactoryOpArray = nsTArray>; StaticAutoPtr gFactoryOps; // Maps a database id to information about live database actors. using DatabaseActorHashtable = nsClassHashtable; StaticAutoPtr gLiveDatabaseHashtable; StaticRefPtr gConnectionPool; using DatabaseLoggingInfoHashtable = nsTHashMap; StaticAutoPtr gLoggingInfoHashtable; using TelemetryIdHashtable = nsTHashMap; StaticAutoPtr gTelemetryIdHashtable; // Protects all reads and writes to gTelemetryIdHashtable. StaticAutoPtr gTelemetryIdMutex; // For private browsing, maps the raw database names provided by content to a // replacement UUID in order to avoid exposing the name of the database on // disk or a directly derived value, such as the non-private-browsing // representation. This mapping will be the same for all databases with the // same name across all storage keys/origins for the lifetime of the IDB // QuotaClient. In tests, the QuotaClient may be created and destroyed multiple // times, but for normal browser use the QuotaClient will last until the // browser shuts down. Bug 1831835 will improve this implementation to avoid // using the same mapping across storage keys and to deal with the resulting // lifecycle issues of the additional memory use. using StorageDatabaseNameHashtable = nsTHashMap; StaticAutoPtr gStorageDatabaseNameHashtable; // Protects all reads and writes to gStorageDatabaseNameHashtable. StaticAutoPtr gStorageDatabaseNameMutex; #ifdef DEBUG StaticRefPtr gDEBUGThreadSlower; #endif // DEBUG void IncreaseBusyCount() { AssertIsOnBackgroundThread(); // If this is the first instance then we need to do some initialization. if (!gBusyCount) { MOZ_ASSERT(!gFactoryOps); gFactoryOps = new FactoryOpArray(); MOZ_ASSERT(!gLiveDatabaseHashtable); gLiveDatabaseHashtable = new DatabaseActorHashtable(); MOZ_ASSERT(!gLoggingInfoHashtable); gLoggingInfoHashtable = new DatabaseLoggingInfoHashtable(); #ifdef DEBUG if (kDEBUGThreadPriority != nsISupportsPriority::PRIORITY_NORMAL) { NS_WARNING( "PBackground thread debugging enabled, priority has been " "modified!"); nsCOMPtr thread = do_QueryInterface(NS_GetCurrentThread()); MOZ_ASSERT(thread); MOZ_ALWAYS_SUCCEEDS(thread->SetPriority(kDEBUGThreadPriority)); } if (kDEBUGThreadSleepMS) { NS_WARNING( "PBackground thread debugging enabled, sleeping after every " "event!"); nsCOMPtr thread = do_QueryInterface(NS_GetCurrentThread()); MOZ_ASSERT(thread); gDEBUGThreadSlower = new DEBUGThreadSlower(); MOZ_ALWAYS_SUCCEEDS(thread->AddObserver(gDEBUGThreadSlower)); } #endif // DEBUG } gBusyCount++; } void DecreaseBusyCount() { AssertIsOnBackgroundThread(); MOZ_ASSERT(gBusyCount); // Clean up if there are no more instances. if (--gBusyCount == 0) { MOZ_ASSERT(gLoggingInfoHashtable); gLoggingInfoHashtable = nullptr; MOZ_ASSERT(gLiveDatabaseHashtable); MOZ_ASSERT(!gLiveDatabaseHashtable->Count()); gLiveDatabaseHashtable = nullptr; MOZ_ASSERT(gFactoryOps); MOZ_ASSERT(gFactoryOps->IsEmpty()); gFactoryOps = nullptr; #ifdef DEBUG if (kDEBUGThreadPriority != nsISupportsPriority::PRIORITY_NORMAL) { nsCOMPtr thread = do_QueryInterface(NS_GetCurrentThread()); MOZ_ASSERT(thread); MOZ_ALWAYS_SUCCEEDS( thread->SetPriority(nsISupportsPriority::PRIORITY_NORMAL)); } if (kDEBUGThreadSleepMS) { MOZ_ASSERT(gDEBUGThreadSlower); nsCOMPtr thread = do_QueryInterface(NS_GetCurrentThread()); MOZ_ASSERT(thread); MOZ_ALWAYS_SUCCEEDS(thread->RemoveObserver(gDEBUGThreadSlower)); gDEBUGThreadSlower = nullptr; } #endif // DEBUG } } template void InvalidateLiveDatabasesMatching(const Condition& aCondition) { AssertIsOnBackgroundThread(); if (!gLiveDatabaseHashtable) { return; } // Invalidating a Database will cause it to be removed from the // gLiveDatabaseHashtable entries' mLiveDatabases, and, if it was the last // element in mLiveDatabases, to remove the whole hashtable entry. Therefore, // we need to make a temporary list of the databases to invalidate to avoid // iterator invalidation. nsTArray> databases; for (const auto& liveDatabasesEntry : gLiveDatabaseHashtable->Values()) { for (const auto& database : liveDatabasesEntry->mLiveDatabases) { if (aCondition(*database)) { databases.AppendElement( SafeRefPtr{database.get(), AcquireStrongRefFromRawPtr{}}); } } } for (const auto& database : databases) { database->Invalidate(); } } uint32_t TelemetryIdForFile(nsIFile* aFile) { // May be called on any thread! MOZ_ASSERT(aFile); MOZ_ASSERT(gTelemetryIdMutex); // The storage directory is structured like this: // // /storage///idb/.sqlite // // For the purposes of this function we're only concerned with the // , , and pieces. nsString filename; MOZ_ALWAYS_SUCCEEDS(aFile->GetLeafName(filename)); // Make sure we were given a database file. MOZ_ASSERT(StringEndsWith(filename, kSQLiteSuffix)); filename.Truncate(filename.Length() - kSQLiteSuffix.Length()); // Get the "idb" directory. nsCOMPtr idbDirectory; MOZ_ALWAYS_SUCCEEDS(aFile->GetParent(getter_AddRefs(idbDirectory))); DebugOnly idbLeafName; MOZ_ASSERT(NS_SUCCEEDED(idbDirectory->GetLeafName(idbLeafName))); MOZ_ASSERT(static_cast(idbLeafName).EqualsLiteral("idb")); // Get the directory. nsCOMPtr originDirectory; MOZ_ALWAYS_SUCCEEDS(idbDirectory->GetParent(getter_AddRefs(originDirectory))); nsString origin; MOZ_ALWAYS_SUCCEEDS(originDirectory->GetLeafName(origin)); // Any databases in these directories are owned by the application and should // not have their filenames masked. Hopefully they also appear in the // Telemetry.cpp whitelist. if (origin.EqualsLiteral("chrome") || origin.EqualsLiteral("moz-safe-about+home")) { return 0; } // Get the directory. nsCOMPtr persistenceDirectory; MOZ_ALWAYS_SUCCEEDS( originDirectory->GetParent(getter_AddRefs(persistenceDirectory))); nsString persistence; MOZ_ALWAYS_SUCCEEDS(persistenceDirectory->GetLeafName(persistence)); constexpr auto separator = u"*"_ns; uint32_t hashValue = HashString(persistence + separator + origin + separator + filename); MutexAutoLock lock(*gTelemetryIdMutex); if (!gTelemetryIdHashtable) { gTelemetryIdHashtable = new TelemetryIdHashtable(); } return gTelemetryIdHashtable->LookupOrInsertWith(hashValue, [] { static uint32_t sNextId = 1; // We're locked, no need for atomics. return sNextId++; }); } nsAutoString GetDatabaseFilenameBase(const nsAString& aDatabaseName, bool aIsPrivate) { nsAutoString databaseFilenameBase; if (aIsPrivate) { MOZ_DIAGNOSTIC_ASSERT(gStorageDatabaseNameMutex); MutexAutoLock lock(*gStorageDatabaseNameMutex); if (!gStorageDatabaseNameHashtable) { gStorageDatabaseNameHashtable = new StorageDatabaseNameHashtable(); } databaseFilenameBase.Append( gStorageDatabaseNameHashtable->LookupOrInsertWith(aDatabaseName, []() { return NSID_TrimBracketsUTF16(nsID::GenerateUUID()); })); return databaseFilenameBase; } // WARNING: do not change this hash function. See the comment in HashName() // for details. databaseFilenameBase.AppendInt(HashName(aDatabaseName)); nsAutoCString escapedName; if (!NS_Escape(NS_ConvertUTF16toUTF8(aDatabaseName), escapedName, url_XPAlphas)) { MOZ_CRASH("Can't escape database name!"); } const char* forwardIter = escapedName.BeginReading(); const char* backwardIter = escapedName.EndReading() - 1; nsAutoCString substring; while (forwardIter <= backwardIter && substring.Length() < 21) { if (substring.Length() % 2) { substring.Append(*backwardIter--); } else { substring.Append(*forwardIter++); } } databaseFilenameBase.AppendASCII(substring.get(), substring.Length()); return databaseFilenameBase; } const CommonIndexOpenCursorParams& GetCommonIndexOpenCursorParams( const OpenCursorParams& aParams) { switch (aParams.type()) { case OpenCursorParams::TIndexOpenCursorParams: return aParams.get_IndexOpenCursorParams().commonIndexParams(); case OpenCursorParams::TIndexOpenKeyCursorParams: return aParams.get_IndexOpenKeyCursorParams().commonIndexParams(); default: MOZ_CRASH("Should never get here!"); } } const CommonOpenCursorParams& GetCommonOpenCursorParams( const OpenCursorParams& aParams) { switch (aParams.type()) { case OpenCursorParams::TObjectStoreOpenCursorParams: return aParams.get_ObjectStoreOpenCursorParams().commonParams(); case OpenCursorParams::TObjectStoreOpenKeyCursorParams: return aParams.get_ObjectStoreOpenKeyCursorParams().commonParams(); case OpenCursorParams::TIndexOpenCursorParams: case OpenCursorParams::TIndexOpenKeyCursorParams: return GetCommonIndexOpenCursorParams(aParams).commonParams(); default: MOZ_CRASH("Should never get here!"); } } // TODO: Using nsCString as a return type here seems to lead to a dependency on // some temporaries, which I did not expect. Is it a good idea that the default // operator+ behaviour constructs such strings? It is certainly useful as an // optimization, but this should be better done via an appropriately named // function rather than an operator. nsAutoCString MakeColumnPairSelectionList( const nsLiteralCString& aPlainColumnName, const nsLiteralCString& aLocaleAwareColumnName, const nsLiteralCString& aSortColumnAlias, const bool aIsLocaleAware) { return aPlainColumnName + (aIsLocaleAware ? EmptyCString() : " as "_ns + aSortColumnAlias) + ", "_ns + aLocaleAwareColumnName + (aIsLocaleAware ? " as "_ns + aSortColumnAlias : EmptyCString()); } constexpr bool IsIncreasingOrder(const IDBCursorDirection aDirection) { MOZ_ASSERT(aDirection == IDBCursorDirection::Next || aDirection == IDBCursorDirection::Nextunique || aDirection == IDBCursorDirection::Prev || aDirection == IDBCursorDirection::Prevunique); return aDirection == IDBCursorDirection::Next || aDirection == IDBCursorDirection::Nextunique; } constexpr bool IsUnique(const IDBCursorDirection aDirection) { MOZ_ASSERT(aDirection == IDBCursorDirection::Next || aDirection == IDBCursorDirection::Nextunique || aDirection == IDBCursorDirection::Prev || aDirection == IDBCursorDirection::Prevunique); return aDirection == IDBCursorDirection::Nextunique || aDirection == IDBCursorDirection::Prevunique; } // TODO: In principle, this could be constexpr, if operator+(nsLiteralCString, // nsLiteralCString) were constexpr and returned a literal type. nsAutoCString MakeDirectionClause(const IDBCursorDirection aDirection) { return " ORDER BY "_ns + kColumnNameKey + (IsIncreasingOrder(aDirection) ? " ASC"_ns : " DESC"_ns); } enum struct ComparisonOperator { LessThan, LessOrEquals, Equals, GreaterThan, GreaterOrEquals, }; constexpr nsLiteralCString GetComparisonOperatorString( const ComparisonOperator aComparisonOperator) { switch (aComparisonOperator) { case ComparisonOperator::LessThan: return "<"_ns; case ComparisonOperator::LessOrEquals: return "<="_ns; case ComparisonOperator::Equals: return "=="_ns; case ComparisonOperator::GreaterThan: return ">"_ns; case ComparisonOperator::GreaterOrEquals: return ">="_ns; } // TODO: This is just to silence the "control reaches end of non-void // function" warning. Cannot use MOZ_CRASH in a constexpr function, // unfortunately. return ""_ns; } nsAutoCString GetKeyClause(const nsACString& aColumnName, const ComparisonOperator aComparisonOperator, const nsLiteralCString& aStmtParamName) { return aColumnName + " "_ns + GetComparisonOperatorString(aComparisonOperator) + " :"_ns + aStmtParamName; } nsAutoCString GetSortKeyClause(const ComparisonOperator aComparisonOperator, const nsLiteralCString& aStmtParamName) { return GetKeyClause(kColumnNameAliasSortKey, aComparisonOperator, aStmtParamName); } template struct PopulateResponseHelper; struct CommonPopulateResponseHelper { explicit CommonPopulateResponseHelper( const TransactionDatabaseOperationBase& aOp) : mOp{aOp} {} nsresult GetKeys(mozIStorageStatement* const aStmt, Key* const aOptOutSortKey) { QM_TRY(MOZ_TO_RESULT(GetCommonKeys(aStmt))); if (aOptOutSortKey) { *aOptOutSortKey = mPosition; } return NS_OK; } nsresult GetCommonKeys(mozIStorageStatement* const aStmt) { MOZ_ASSERT(mPosition.IsUnset()); QM_TRY(MOZ_TO_RESULT(mPosition.SetFromStatement(aStmt, 0))); IDB_LOG_MARK_PARENT_TRANSACTION_REQUEST( "PRELOAD: Populating response with key %s", "Populating%.0s", IDB_LOG_ID_STRING(mOp.BackgroundChildLoggingId()), mOp.TransactionLoggingSerialNumber(), mOp.LoggingSerialNumber(), mPosition.GetBuffer().get()); return NS_OK; } template void FillKeys(Response& aResponse) { MOZ_ASSERT(!mPosition.IsUnset()); aResponse.key() = std::move(mPosition); } template static size_t GetKeySize(const Response& aResponse) { return aResponse.key().GetBuffer().Length(); } protected: const Key& GetPosition() const { return mPosition; } private: const TransactionDatabaseOperationBase& mOp; Key mPosition; }; struct IndexPopulateResponseHelper : CommonPopulateResponseHelper { using CommonPopulateResponseHelper::CommonPopulateResponseHelper; nsresult GetKeys(mozIStorageStatement* const aStmt, Key* const aOptOutSortKey) { MOZ_ASSERT(mLocaleAwarePosition.IsUnset()); MOZ_ASSERT(mObjectStorePosition.IsUnset()); QM_TRY(MOZ_TO_RESULT(CommonPopulateResponseHelper::GetCommonKeys(aStmt))); QM_TRY(MOZ_TO_RESULT(mLocaleAwarePosition.SetFromStatement(aStmt, 1))); QM_TRY(MOZ_TO_RESULT(mObjectStorePosition.SetFromStatement(aStmt, 2))); if (aOptOutSortKey) { *aOptOutSortKey = mLocaleAwarePosition.IsUnset() ? GetPosition() : mLocaleAwarePosition; } return NS_OK; } template void FillKeys(Response& aResponse) { MOZ_ASSERT(!mLocaleAwarePosition.IsUnset()); MOZ_ASSERT(!mObjectStorePosition.IsUnset()); CommonPopulateResponseHelper::FillKeys(aResponse); aResponse.sortKey() = std::move(mLocaleAwarePosition); aResponse.objectKey() = std::move(mObjectStorePosition); } template static size_t GetKeySize(Response& aResponse) { return CommonPopulateResponseHelper::GetKeySize(aResponse) + aResponse.sortKey().GetBuffer().Length() + aResponse.objectKey().GetBuffer().Length(); } private: Key mLocaleAwarePosition, mObjectStorePosition; }; struct KeyPopulateResponseHelper { static constexpr nsresult MaybeGetCloneInfo( mozIStorageStatement* const /*aStmt*/, const CursorBase& /*aCursor*/) { return NS_OK; } template static constexpr void MaybeFillCloneInfo(Response& /*aResponse*/, FilesArray* const /*aFiles*/) {} template static constexpr size_t MaybeGetCloneInfoSize(const Response& /*aResponse*/) { return 0; } }; template struct ValuePopulateResponseHelper { nsresult MaybeGetCloneInfo(mozIStorageStatement* const aStmt, const ValueCursorBase& aCursor) { constexpr auto offset = StatementHasIndexKeyBindings ? 2 : 0; QM_TRY_UNWRAP(auto cloneInfo, GetStructuredCloneReadInfoFromStatement( aStmt, 2 + offset, 1 + offset, *aCursor.mFileManager)); mCloneInfo.init(std::move(cloneInfo)); if (mCloneInfo->HasPreprocessInfo()) { IDB_WARNING("Preprocessing for cursors not yet implemented!"); return NS_ERROR_NOT_IMPLEMENTED; } return NS_OK; } template void MaybeFillCloneInfo(Response& aResponse, FilesArray* const aFiles) { auto cloneInfo = mCloneInfo.release(); aResponse.cloneInfo().data().data = cloneInfo.ReleaseData(); aFiles->AppendElement(cloneInfo.ReleaseFiles()); } template static size_t MaybeGetCloneInfoSize(const Response& aResponse) { return aResponse.cloneInfo().data().data.Size(); } private: LazyInitializedOnceEarlyDestructible mCloneInfo; }; template <> struct PopulateResponseHelper : ValuePopulateResponseHelper, CommonPopulateResponseHelper { using CommonPopulateResponseHelper::CommonPopulateResponseHelper; static auto& GetTypedResponse(CursorResponse* const aResponse) { return aResponse->get_ArrayOfObjectStoreCursorResponse(); } }; template <> struct PopulateResponseHelper : KeyPopulateResponseHelper, CommonPopulateResponseHelper { using CommonPopulateResponseHelper::CommonPopulateResponseHelper; static auto& GetTypedResponse(CursorResponse* const aResponse) { return aResponse->get_ArrayOfObjectStoreKeyCursorResponse(); } }; template <> struct PopulateResponseHelper : ValuePopulateResponseHelper, IndexPopulateResponseHelper { using IndexPopulateResponseHelper::IndexPopulateResponseHelper; static auto& GetTypedResponse(CursorResponse* const aResponse) { return aResponse->get_ArrayOfIndexCursorResponse(); } }; template <> struct PopulateResponseHelper : KeyPopulateResponseHelper, IndexPopulateResponseHelper { using IndexPopulateResponseHelper::IndexPopulateResponseHelper; static auto& GetTypedResponse(CursorResponse* const aResponse) { return aResponse->get_ArrayOfIndexKeyCursorResponse(); } }; nsresult DispatchAndReturnFileReferences( PersistenceType aPersistenceType, const nsACString& aOrigin, const nsAString& aDatabaseName, const int64_t aFileId, int32_t* const aMemRefCnt, int32_t* const aDBRefCnt, bool* const aResult) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aMemRefCnt); MOZ_ASSERT(aDBRefCnt); MOZ_ASSERT(aResult); *aResult = false; *aMemRefCnt = -1; *aDBRefCnt = -1; mozilla::Monitor monitor MOZ_ANNOTATED(__func__); bool waiting = true; auto lambda = [&] { AssertIsOnIOThread(); { IndexedDatabaseManager* const mgr = IndexedDatabaseManager::Get(); MOZ_ASSERT(mgr); const SafeRefPtr fileManager = mgr->GetFileManager(aPersistenceType, aOrigin, aDatabaseName); if (fileManager) { const SafeRefPtr fileInfo = fileManager->GetFileInfo(aFileId); if (fileInfo) { fileInfo->GetReferences(aMemRefCnt, aDBRefCnt); if (*aMemRefCnt != -1) { // We added an extra temp ref, so account for that accordingly. (*aMemRefCnt)--; } *aResult = true; } } } mozilla::MonitorAutoLock lock(monitor); MOZ_ASSERT(waiting); waiting = false; lock.Notify(); }; QuotaManager* const quotaManager = QuotaManager::Get(); MOZ_ASSERT(quotaManager); // XXX can't we simply use NS_DispatchAndSpinEventLoopUntilComplete instead of // using a monitor? QM_TRY(MOZ_TO_RESULT(quotaManager->IOThread()->Dispatch( NS_NewRunnableFunction("GetFileReferences", std::move(lambda)), NS_DISPATCH_NORMAL))); mozilla::MonitorAutoLock autolock(monitor); while (waiting) { autolock.Wait(); } return NS_OK; } class DeserializeIndexValueHelper final : public Runnable { public: DeserializeIndexValueHelper(int64_t aIndexID, const KeyPath& aKeyPath, bool aMultiEntry, const nsACString& aLocale, StructuredCloneReadInfoParent& aCloneReadInfo, nsTArray& aUpdateInfoArray) : Runnable("DeserializeIndexValueHelper"), mMonitor("DeserializeIndexValueHelper::mMonitor"), mIndexID(aIndexID), mKeyPath(aKeyPath), mMultiEntry(aMultiEntry), mLocale(aLocale), mCloneReadInfo(aCloneReadInfo), mUpdateInfoArray(aUpdateInfoArray), mStatus(NS_ERROR_FAILURE) {} nsresult DispatchAndWait() { // FIXME(Bug 1637530) Re-enable optimization using a non-system-principaled // JS context #if 0 // We don't need to go to the main-thread and use the sandbox. Let's create // the updateInfo data here. if (!mCloneReadInfo.Data().Size()) { AutoJSAPI jsapi; jsapi.Init(); JS::Rooted value(jsapi.cx()); value.setUndefined(); ErrorResult rv; IDBObjectStore::AppendIndexUpdateInfo(mIndexID, mKeyPath, mMultiEntry, mLocale, jsapi.cx(), value, &mUpdateInfoArray, &rv); return rv.Failed() ? rv.StealNSResult() : NS_OK; } #endif // The operation will continue on the main-thread. MOZ_ASSERT(!(mCloneReadInfo.Data().Size() % sizeof(uint64_t))); MonitorAutoLock lock(mMonitor); RefPtr self = this; QM_TRY(MOZ_TO_RESULT(SchedulerGroup::Dispatch(self.forget()))); lock.Wait(); return mStatus; } NS_IMETHOD Run() override { MOZ_ASSERT(NS_IsMainThread()); AutoJSAPI jsapi; jsapi.Init(); JSContext* const cx = jsapi.cx(); JS::Rooted global(cx, GetSandbox(cx)); QM_TRY(OkIf(global), NS_OK, [this](const NotOk) { OperationCompleted(NS_ERROR_FAILURE); }); const JSAutoRealm ar(cx, global); JS::Rooted value(cx); QM_TRY(MOZ_TO_RESULT(DeserializeIndexValue(cx, &value)), NS_OK, [this](const nsresult rv) { OperationCompleted(rv); }); ErrorResult errorResult; IDBObjectStore::AppendIndexUpdateInfo(mIndexID, mKeyPath, mMultiEntry, mLocale, cx, value, &mUpdateInfoArray, &errorResult); QM_TRY(OkIf(!errorResult.Failed()), NS_OK, ([this, &errorResult](const NotOk) { OperationCompleted(errorResult.StealNSResult()); })); OperationCompleted(NS_OK); return NS_OK; } private: nsresult DeserializeIndexValue(JSContext* aCx, JS::MutableHandle aValue) { static const JSStructuredCloneCallbacks callbacks = { StructuredCloneReadCallback, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}; if (!JS_ReadStructuredClone( aCx, mCloneReadInfo.Data(), JS_STRUCTURED_CLONE_VERSION, JS::StructuredCloneScope::DifferentProcessForIndexedDB, aValue, JS::CloneDataPolicy(), &callbacks, &mCloneReadInfo)) { return NS_ERROR_DOM_DATA_CLONE_ERR; } return NS_OK; } void OperationCompleted(nsresult aStatus) { mStatus = aStatus; MonitorAutoLock lock(mMonitor); lock.Notify(); } Monitor mMonitor MOZ_UNANNOTATED; const int64_t mIndexID; const KeyPath& mKeyPath; const bool mMultiEntry; const nsCString mLocale; StructuredCloneReadInfoParent& mCloneReadInfo; nsTArray& mUpdateInfoArray; nsresult mStatus; }; auto DeserializeIndexValueToUpdateInfos( int64_t aIndexID, const KeyPath& aKeyPath, bool aMultiEntry, const nsACString& aLocale, StructuredCloneReadInfoParent& aCloneReadInfo) { MOZ_ASSERT(!NS_IsMainThread()); using ArrayType = AutoTArray; using ResultType = Result; ArrayType updateInfoArray; const auto helper = MakeRefPtr( aIndexID, aKeyPath, aMultiEntry, aLocale, aCloneReadInfo, updateInfoArray); const nsresult rv = helper->DispatchAndWait(); return NS_FAILED(rv) ? Err(rv) : ResultType{std::move(updateInfoArray)}; } bool IsSome( const Maybe& aMaybeStmt) { return aMaybeStmt.isSome(); } already_AddRefed MakeConnectionIOTarget() { nsCOMPtr threadPool = new nsThreadPool(); MOZ_ALWAYS_SUCCEEDS(threadPool->SetThreadLimit(kMaxConnectionThreadCount)); MOZ_ALWAYS_SUCCEEDS( threadPool->SetIdleThreadLimit(kMaxIdleConnectionThreadCount)); MOZ_ALWAYS_SUCCEEDS( threadPool->SetIdleThreadTimeout(kConnectionThreadIdleMS)); MOZ_ALWAYS_SUCCEEDS(threadPool->SetName("IndexedDB IO"_ns)); return threadPool.forget(); } } // namespace /******************************************************************************* * Exported functions ******************************************************************************/ already_AddRefed AllocPBackgroundIDBFactoryParent( const LoggingInfo& aLoggingInfo) { AssertIsOnBackgroundThread(); if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread())) { return nullptr; } if (NS_AUUF_OR_WARN_IF(!aLoggingInfo.nextTransactionSerialNumber()) || NS_AUUF_OR_WARN_IF( !aLoggingInfo.nextVersionChangeTransactionSerialNumber()) || NS_AUUF_OR_WARN_IF(!aLoggingInfo.nextRequestSerialNumber())) { return nullptr; } SafeRefPtr actor = Factory::Create(aLoggingInfo); MOZ_ASSERT(actor); return actor.forget(); } bool RecvPBackgroundIDBFactoryConstructor( PBackgroundIDBFactoryParent* aActor, const LoggingInfo& /* aLoggingInfo */) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); return true; } PBackgroundIndexedDBUtilsParent* AllocPBackgroundIndexedDBUtilsParent() { AssertIsOnBackgroundThread(); RefPtr actor = new Utils(); return actor.forget().take(); } bool DeallocPBackgroundIndexedDBUtilsParent( PBackgroundIndexedDBUtilsParent* aActor) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); RefPtr actor = dont_AddRef(static_cast(aActor)); return true; } bool RecvFlushPendingFileDeletions() { AssertIsOnBackgroundThread(); if (QuotaClient* quotaClient = QuotaClient::GetInstance()) { QM_WARNONLY_TRY(QM_TO_RESULT(quotaClient->FlushPendingFileDeletions())); } return true; } RefPtr CreateQuotaClient() { AssertIsOnBackgroundThread(); return MakeRefPtr(); } nsresult DatabaseFileManager::AsyncDeleteFile(int64_t aFileId) { AssertIsOnBackgroundThread(); MOZ_ASSERT(!mFileInfos.Contains(aFileId)); QuotaClient* quotaClient = QuotaClient::GetInstance(); if (quotaClient) { QM_TRY(MOZ_TO_RESULT(quotaClient->AsyncDeleteFile(this, aFileId))); } return NS_OK; } /******************************************************************************* * DatabaseConnection implementation ******************************************************************************/ DatabaseConnection::DatabaseConnection( MovingNotNull> aStorageConnection, MovingNotNull> aFileManager) : CachingDatabaseConnection(std::move(aStorageConnection)), mFileManager(std::move(aFileManager)), mInReadTransaction(false), mInWriteTransaction(false) #ifdef DEBUG , mDEBUGSavepointCount(0) #endif { AssertIsOnConnectionThread(); MOZ_ASSERT(mFileManager); } DatabaseConnection::~DatabaseConnection() { MOZ_ASSERT(!mFileManager); MOZ_ASSERT(!mUpdateRefcountFunction); MOZ_DIAGNOSTIC_ASSERT(!mInWriteTransaction); MOZ_ASSERT(!mDEBUGSavepointCount); } nsresult DatabaseConnection::Init() { AssertIsOnConnectionThread(); MOZ_ASSERT(!mInReadTransaction); MOZ_ASSERT(!mInWriteTransaction); QM_TRY(MOZ_TO_RESULT(ExecuteCachedStatement("BEGIN;"_ns))); mInReadTransaction = true; return NS_OK; } nsresult DatabaseConnection::BeginWriteTransaction() { AssertIsOnConnectionThread(); MOZ_ASSERT(HasStorageConnection()); MOZ_ASSERT(mInReadTransaction); MOZ_ASSERT(!mInWriteTransaction); AUTO_PROFILER_LABEL("DatabaseConnection::BeginWriteTransaction", DOM); // Release our read locks. QM_TRY(MOZ_TO_RESULT(ExecuteCachedStatement("ROLLBACK;"_ns))); mInReadTransaction = false; if (!mUpdateRefcountFunction) { MOZ_ASSERT(mFileManager); RefPtr function = new UpdateRefcountFunction(this, **mFileManager); QM_TRY(MOZ_TO_RESULT(MutableStorageConnection().CreateFunction( "update_refcount"_ns, /* aNumArguments */ 2, function))); mUpdateRefcountFunction = std::move(function); } // This one cannot obviously use ExecuteCachedStatement because of the custom // error handling for Execute only. If only Execute can produce // NS_ERROR_STORAGE_BUSY, we could actually use ExecuteCachedStatement and // simplify this. QM_TRY_INSPECT(const auto& beginStmt, BorrowCachedStatement("BEGIN IMMEDIATE;"_ns)); QM_TRY(QM_OR_ELSE_WARN_IF( // Expression. MOZ_TO_RESULT(beginStmt->Execute()), // Predicate. IsSpecificError, // Fallback. ([&beginStmt](nsresult rv) { NS_WARNING( "Received NS_ERROR_STORAGE_BUSY when attempting to start write " "transaction, retrying for up to 10 seconds"); // Another thread must be using the database. Wait up to 10 seconds // for that to complete. const TimeStamp start = TimeStamp::NowLoRes(); while (true) { PR_Sleep(PR_MillisecondsToInterval(100)); rv = beginStmt->Execute(); if (rv != NS_ERROR_STORAGE_BUSY || TimeStamp::NowLoRes() - start > TimeDuration::FromSeconds(10)) { break; } } return MOZ_TO_RESULT(rv); }))); mInWriteTransaction = true; return NS_OK; } nsresult DatabaseConnection::CommitWriteTransaction() { AssertIsOnConnectionThread(); MOZ_ASSERT(HasStorageConnection()); MOZ_ASSERT(!mInReadTransaction); MOZ_ASSERT(mInWriteTransaction); AUTO_PROFILER_LABEL("DatabaseConnection::CommitWriteTransaction", DOM); QM_TRY(MOZ_TO_RESULT(ExecuteCachedStatement("COMMIT;"_ns))); mInWriteTransaction = false; return NS_OK; } void DatabaseConnection::RollbackWriteTransaction() { AssertIsOnConnectionThread(); MOZ_ASSERT(!mInReadTransaction); MOZ_DIAGNOSTIC_ASSERT(HasStorageConnection()); AUTO_PROFILER_LABEL("DatabaseConnection::RollbackWriteTransaction", DOM); if (!mInWriteTransaction) { return; } QM_WARNONLY_TRY( BorrowCachedStatement("ROLLBACK;"_ns) .andThen([&self = *this](const auto& stmt) -> Result { // This may fail if SQLite already rolled back the transaction // so ignore any errors. // XXX ROLLBACK can fail quite normmally if a previous statement // failed to execute successfully so SQLite rolled back the // transaction already. However, if it failed because of some other // reason, we could try to close the connection. Unused << stmt->Execute(); self.mInWriteTransaction = false; return Ok{}; })); } void DatabaseConnection::FinishWriteTransaction() { AssertIsOnConnectionThread(); MOZ_ASSERT(HasStorageConnection()); MOZ_ASSERT(!mInReadTransaction); MOZ_ASSERT(!mInWriteTransaction); AUTO_PROFILER_LABEL("DatabaseConnection::FinishWriteTransaction", DOM); if (mUpdateRefcountFunction) { mUpdateRefcountFunction->Reset(); } QM_WARNONLY_TRY(MOZ_TO_RESULT(ExecuteCachedStatement("BEGIN;"_ns)) .andThen([&](const auto) -> Result { mInReadTransaction = true; return Ok{}; })); } nsresult DatabaseConnection::StartSavepoint() { AssertIsOnConnectionThread(); MOZ_ASSERT(HasStorageConnection()); MOZ_ASSERT(mUpdateRefcountFunction); MOZ_ASSERT(mInWriteTransaction); AUTO_PROFILER_LABEL("DatabaseConnection::StartSavepoint", DOM); QM_TRY(MOZ_TO_RESULT(ExecuteCachedStatement(SAVEPOINT_CLAUSE))); mUpdateRefcountFunction->StartSavepoint(); #ifdef DEBUG MOZ_ASSERT(mDEBUGSavepointCount < UINT32_MAX); mDEBUGSavepointCount++; #endif return NS_OK; } nsresult DatabaseConnection::ReleaseSavepoint() { AssertIsOnConnectionThread(); MOZ_ASSERT(HasStorageConnection()); MOZ_ASSERT(mUpdateRefcountFunction); MOZ_ASSERT(mInWriteTransaction); AUTO_PROFILER_LABEL("DatabaseConnection::ReleaseSavepoint", DOM); QM_TRY(MOZ_TO_RESULT(ExecuteCachedStatement("RELEASE "_ns SAVEPOINT_CLAUSE))); mUpdateRefcountFunction->ReleaseSavepoint(); #ifdef DEBUG MOZ_ASSERT(mDEBUGSavepointCount); mDEBUGSavepointCount--; #endif return NS_OK; } nsresult DatabaseConnection::RollbackSavepoint() { AssertIsOnConnectionThread(); MOZ_ASSERT(HasStorageConnection()); MOZ_ASSERT(mUpdateRefcountFunction); MOZ_ASSERT(mInWriteTransaction); AUTO_PROFILER_LABEL("DatabaseConnection::RollbackSavepoint", DOM); #ifdef DEBUG MOZ_ASSERT(mDEBUGSavepointCount); mDEBUGSavepointCount--; #endif mUpdateRefcountFunction->RollbackSavepoint(); QM_TRY_INSPECT(const auto& stmt, BorrowCachedStatement("ROLLBACK TO "_ns SAVEPOINT_CLAUSE)); // This may fail if SQLite already rolled back the savepoint so ignore any // errors. Unused << stmt->Execute(); return NS_OK; } nsresult DatabaseConnection::CheckpointInternal(CheckpointMode aMode) { AssertIsOnConnectionThread(); MOZ_ASSERT(!mInReadTransaction); MOZ_ASSERT(!mInWriteTransaction); AUTO_PROFILER_LABEL("DatabaseConnection::CheckpointInternal", DOM); nsAutoCString stmtString; stmtString.AssignLiteral("PRAGMA wal_checkpoint("); switch (aMode) { case CheckpointMode::Full: // Ensures that the database is completely checkpointed and flushed to // disk. stmtString.AppendLiteral("FULL"); break; case CheckpointMode::Restart: // Like Full, but also ensures that the next write will start overwriting // the existing WAL file rather than letting the WAL file grow. stmtString.AppendLiteral("RESTART"); break; case CheckpointMode::Truncate: // Like Restart but also truncates the existing WAL file. stmtString.AppendLiteral("TRUNCATE"); break; default: MOZ_CRASH("Unknown CheckpointMode!"); } stmtString.AppendLiteral(");"); QM_TRY(MOZ_TO_RESULT(ExecuteCachedStatement(stmtString))); return NS_OK; } void DatabaseConnection::DoIdleProcessing(bool aNeedsCheckpoint) { AssertIsOnConnectionThread(); MOZ_ASSERT(mInReadTransaction); MOZ_ASSERT(!mInWriteTransaction); AUTO_PROFILER_LABEL("DatabaseConnection::DoIdleProcessing", DOM); CachingDatabaseConnection::CachedStatement freelistStmt; const uint32_t freelistCount = [this, &freelistStmt] { QM_TRY_RETURN(GetFreelistCount(freelistStmt), 0u); }(); CachedStatement rollbackStmt; CachedStatement beginStmt; if (aNeedsCheckpoint || freelistCount) { QM_TRY_UNWRAP(rollbackStmt, GetCachedStatement("ROLLBACK;"_ns), QM_VOID); QM_TRY_UNWRAP(beginStmt, GetCachedStatement("BEGIN;"_ns), QM_VOID); // Release the connection's normal transaction. It's possible that it could // fail, but that isn't a problem here. Unused << rollbackStmt.Borrow()->Execute(); mInReadTransaction = false; } const bool freedSomePages = freelistCount && [this, &freelistStmt, &rollbackStmt, freelistCount, aNeedsCheckpoint] { // Warn in case of an error, but do not propagate it. Just indicate we // didn't free any pages. QM_TRY_INSPECT(const bool& res, ReclaimFreePagesWhileIdle(freelistStmt, rollbackStmt, freelistCount, aNeedsCheckpoint), false); // Make sure we didn't leave a transaction running. MOZ_ASSERT(!mInReadTransaction); MOZ_ASSERT(!mInWriteTransaction); return res; }(); // Truncate the WAL if we were asked to or if we managed to free some space. if (aNeedsCheckpoint || freedSomePages) { QM_WARNONLY_TRY(QM_TO_RESULT(CheckpointInternal(CheckpointMode::Truncate))); } // Finally try to restart the read transaction if we rolled it back earlier. if (beginStmt) { QM_WARNONLY_TRY( MOZ_TO_RESULT(beginStmt.Borrow()->Execute()) .andThen([&self = *this](const Ok) -> Result { self.mInReadTransaction = true; return Ok{}; })); } } Result DatabaseConnection::ReclaimFreePagesWhileIdle( CachedStatement& aFreelistStatement, CachedStatement& aRollbackStatement, uint32_t aFreelistCount, bool aNeedsCheckpoint) { AssertIsOnConnectionThread(); MOZ_ASSERT(aFreelistStatement); MOZ_ASSERT(aRollbackStatement); MOZ_ASSERT(aFreelistCount); MOZ_ASSERT(!mInReadTransaction); MOZ_ASSERT(!mInWriteTransaction); AUTO_PROFILER_LABEL("DatabaseConnection::ReclaimFreePagesWhileIdle", DOM); // Make sure we don't keep working if anything else needs this thread. nsIThread* currentThread = NS_GetCurrentThread(); MOZ_ASSERT(currentThread); if (NS_HasPendingEvents(currentThread)) { return false; } // Make all the statements we'll need up front. // Only try to free 10% at a time so that we can bail out if this connection // suddenly becomes active or if the thread is needed otherwise. QM_TRY_INSPECT( const auto& incrementalVacuumStmt, GetCachedStatement( "PRAGMA incremental_vacuum("_ns + IntToCString(std::max(uint64_t(1), uint64_t(aFreelistCount / 10))) + ");"_ns)); QM_TRY_INSPECT(const auto& beginImmediateStmt, GetCachedStatement("BEGIN IMMEDIATE;"_ns)); QM_TRY_INSPECT(const auto& commitStmt, GetCachedStatement("COMMIT;"_ns)); if (aNeedsCheckpoint) { // Freeing pages is a journaled operation, so it will require additional WAL // space. However, we're idle and are about to checkpoint anyway, so doing a // RESTART checkpoint here should allow us to reuse any existing space. QM_TRY(MOZ_TO_RESULT(CheckpointInternal(CheckpointMode::Restart))); } // Start the write transaction. QM_TRY(MOZ_TO_RESULT(beginImmediateStmt.Borrow()->Execute())); mInWriteTransaction = true; bool freedSomePages = false, interrupted = false; const auto rollback = [&aRollbackStatement, this](const auto&) { MOZ_ASSERT(mInWriteTransaction); // Something failed, make sure we roll everything back. Unused << aRollbackStatement.Borrow()->Execute(); // XXX Is rollback infallible? Shouldn't we check the result? mInWriteTransaction = false; }; uint64_t previousFreelistCount = (uint64_t)aFreelistCount + 1; QM_TRY(CollectWhile( [&aFreelistCount, &previousFreelistCount, &interrupted, currentThread]() -> Result { if (NS_HasPendingEvents(currentThread)) { // Abort if something else wants to use the thread, and // roll back this transaction. It's ok if we never make // progress here because the idle service should // eventually reclaim this space. interrupted = true; return false; } // If we were not able to free anything, we might either see // a DB that has no auto-vacuum support at all or some other // (hopefully temporary) condition that prevents vacuum from // working. Just carry on in non-DEBUG. bool madeProgress = previousFreelistCount != aFreelistCount; previousFreelistCount = aFreelistCount; MOZ_ASSERT(madeProgress); QM_WARNONLY_TRY(MOZ_TO_RESULT(!madeProgress)); return madeProgress && (aFreelistCount != 0); }, [&aFreelistStatement, &aFreelistCount, &incrementalVacuumStmt, &freedSomePages, this]() -> mozilla::Result { QM_TRY(MOZ_TO_RESULT(incrementalVacuumStmt.Borrow()->Execute())); freedSomePages = true; QM_TRY_UNWRAP(aFreelistCount, GetFreelistCount(aFreelistStatement)); return Ok{}; }) .andThen([&commitStmt, &freedSomePages, &interrupted, &rollback, this](Ok) -> Result { if (interrupted) { rollback(Ok{}); freedSomePages = false; } if (freedSomePages) { // Commit the write transaction. QM_TRY(MOZ_TO_RESULT(commitStmt.Borrow()->Execute()), QM_PROPAGATE, [](const auto&) { NS_WARNING("Failed to commit!"); }); mInWriteTransaction = false; } return Ok{}; }), QM_PROPAGATE, rollback); return freedSomePages; } Result DatabaseConnection::GetFreelistCount( CachedStatement& aCachedStatement) { AssertIsOnConnectionThread(); AUTO_PROFILER_LABEL("DatabaseConnection::GetFreelistCount", DOM); if (!aCachedStatement) { QM_TRY_UNWRAP(aCachedStatement, GetCachedStatement("PRAGMA freelist_count;"_ns)); } const auto borrowedStatement = aCachedStatement.Borrow(); QM_TRY_UNWRAP(const DebugOnly hasResult, MOZ_TO_RESULT_INVOKE_MEMBER(&*borrowedStatement, ExecuteStep)); MOZ_ASSERT(hasResult); QM_TRY_INSPECT(const int32_t& freelistCount, MOZ_TO_RESULT_INVOKE_MEMBER(*borrowedStatement, GetInt32, 0)); MOZ_ASSERT(freelistCount >= 0); return uint32_t(freelistCount); } void DatabaseConnection::Close() { AssertIsOnConnectionThread(); MOZ_ASSERT(!mDEBUGSavepointCount); MOZ_DIAGNOSTIC_ASSERT(!mInWriteTransaction); AUTO_PROFILER_LABEL("DatabaseConnection::Close", DOM); if (mUpdateRefcountFunction) { MOZ_ALWAYS_SUCCEEDS( MutableStorageConnection().RemoveFunction("update_refcount"_ns)); mUpdateRefcountFunction = nullptr; } CachingDatabaseConnection::Close(); mFileManager.destroy(); } nsresult DatabaseConnection::DisableQuotaChecks() { AssertIsOnConnectionThread(); MOZ_ASSERT(HasStorageConnection()); if (!mQuotaObject) { MOZ_ASSERT(!mJournalQuotaObject); QM_TRY(MOZ_TO_RESULT(MutableStorageConnection().GetQuotaObjects( getter_AddRefs(mQuotaObject), getter_AddRefs(mJournalQuotaObject)))); MOZ_ASSERT(mQuotaObject); MOZ_ASSERT(mJournalQuotaObject); } mQuotaObject->DisableQuotaCheck(); mJournalQuotaObject->DisableQuotaCheck(); return NS_OK; } void DatabaseConnection::EnableQuotaChecks() { AssertIsOnConnectionThread(); if (!mQuotaObject) { MOZ_ASSERT(!mJournalQuotaObject); // DisableQuotaChecks failed earlier, so we don't need to enable quota // checks again. return; } MOZ_ASSERT(mJournalQuotaObject); const RefPtr quotaObject = std::move(mQuotaObject); const RefPtr journalQuotaObject = std::move(mJournalQuotaObject); quotaObject->EnableQuotaCheck(); journalQuotaObject->EnableQuotaCheck(); QM_TRY_INSPECT(const int64_t& fileSize, GetFileSize(quotaObject->Path()), QM_VOID); QM_TRY_INSPECT(const int64_t& journalFileSize, GetFileSize(journalQuotaObject->Path()), QM_VOID); DebugOnly result = journalQuotaObject->MaybeUpdateSize( journalFileSize, /* aTruncate */ true); MOZ_ASSERT(result); result = quotaObject->MaybeUpdateSize(fileSize, /* aTruncate */ true); MOZ_ASSERT(result); } Result DatabaseConnection::GetFileSize( const nsAString& aPath) { MOZ_ASSERT(!aPath.IsEmpty()); QM_TRY_INSPECT(const auto& file, QM_NewLocalFile(aPath)); QM_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE_MEMBER(file, Exists)); if (exists) { QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(file, GetFileSize)); } return 0; } DatabaseConnection::AutoSavepoint::AutoSavepoint() : mConnection(nullptr) #ifdef DEBUG , mDEBUGTransaction(nullptr) #endif { MOZ_COUNT_CTOR(DatabaseConnection::AutoSavepoint); } DatabaseConnection::AutoSavepoint::~AutoSavepoint() { MOZ_COUNT_DTOR(DatabaseConnection::AutoSavepoint); if (mConnection) { mConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(mDEBUGTransaction); MOZ_ASSERT( mDEBUGTransaction->GetMode() == IDBTransaction::Mode::ReadWrite || mDEBUGTransaction->GetMode() == IDBTransaction::Mode::ReadWriteFlush || mDEBUGTransaction->GetMode() == IDBTransaction::Mode::Cleanup || mDEBUGTransaction->GetMode() == IDBTransaction::Mode::VersionChange); QM_WARNONLY_TRY(QM_TO_RESULT(mConnection->RollbackSavepoint())); } } nsresult DatabaseConnection::AutoSavepoint::Start( const TransactionBase& aTransaction) { MOZ_ASSERT(aTransaction.GetMode() == IDBTransaction::Mode::ReadWrite || aTransaction.GetMode() == IDBTransaction::Mode::ReadWriteFlush || aTransaction.GetMode() == IDBTransaction::Mode::Cleanup || aTransaction.GetMode() == IDBTransaction::Mode::VersionChange); DatabaseConnection* connection = aTransaction.GetDatabase().GetConnection(); MOZ_ASSERT(connection); connection->AssertIsOnConnectionThread(); // The previous operation failed to begin a write transaction and the // following opertion jumped to the connection thread before the previous // operation has updated its failure to the transaction. if (!connection->GetUpdateRefcountFunction()) { NS_WARNING( "The connection was closed because the previous operation " "failed!"); return NS_ERROR_DOM_INDEXEDDB_ABORT_ERR; } MOZ_ASSERT(!mConnection); MOZ_ASSERT(!mDEBUGTransaction); QM_TRY(MOZ_TO_RESULT(connection->StartSavepoint())); mConnection = connection; #ifdef DEBUG mDEBUGTransaction = &aTransaction; #endif return NS_OK; } nsresult DatabaseConnection::AutoSavepoint::Commit() { MOZ_ASSERT(mConnection); mConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(mDEBUGTransaction); QM_TRY(MOZ_TO_RESULT(mConnection->ReleaseSavepoint())); mConnection = nullptr; #ifdef DEBUG mDEBUGTransaction = nullptr; #endif return NS_OK; } DatabaseConnection::UpdateRefcountFunction::UpdateRefcountFunction( DatabaseConnection* const aConnection, DatabaseFileManager& aFileManager) : mConnection(aConnection), mFileManager(aFileManager), mInSavepoint(false) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); } nsresult DatabaseConnection::UpdateRefcountFunction::WillCommit() { MOZ_ASSERT(mConnection); mConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(mConnection->HasStorageConnection()); AUTO_PROFILER_LABEL("DatabaseConnection::UpdateRefcountFunction::WillCommit", DOM); // The parameter names are not used, parameters are bound by index // only locally in the same function. auto update = [updateStatement = LazyStatement{*mConnection, "UPDATE file " "SET refcount = refcount + :delta " "WHERE id = :id"_ns}, selectStatement = LazyStatement{*mConnection, "SELECT id " "FROM file " "WHERE id = :id"_ns}, insertStatement = LazyStatement{ *mConnection, "INSERT INTO file (id, refcount) VALUES(:id, :delta)"_ns}, this](int64_t aId, int32_t aDelta) mutable -> Result { AUTO_PROFILER_LABEL( "DatabaseConnection::UpdateRefcountFunction::WillCommit::Update", DOM); { QM_TRY_INSPECT(const auto& borrowedUpdateStatement, updateStatement.Borrow()); QM_TRY( MOZ_TO_RESULT(borrowedUpdateStatement->BindInt32ByIndex(0, aDelta))); QM_TRY(MOZ_TO_RESULT(borrowedUpdateStatement->BindInt64ByIndex(1, aId))); QM_TRY(MOZ_TO_RESULT(borrowedUpdateStatement->Execute())); } QM_TRY_INSPECT( const int32_t& rows, MOZ_TO_RESULT_INVOKE_MEMBER(mConnection->MutableStorageConnection(), GetAffectedRows)); if (rows > 0) { QM_TRY_INSPECT( const bool& hasResult, selectStatement .BorrowAndExecuteSingleStep( [aId](auto& stmt) -> Result { QM_TRY(MOZ_TO_RESULT(stmt.BindInt64ByIndex(0, aId))); return Ok{}; }) .map(IsSome)); if (!hasResult) { // Don't have to create the journal here, we can create all at once, // just before commit mJournalsToCreateBeforeCommit.AppendElement(aId); } return Ok{}; } QM_TRY_INSPECT(const auto& borrowedInsertStatement, insertStatement.Borrow()); QM_TRY(MOZ_TO_RESULT(borrowedInsertStatement->BindInt64ByIndex(0, aId))); QM_TRY(MOZ_TO_RESULT(borrowedInsertStatement->BindInt32ByIndex(1, aDelta))); QM_TRY(MOZ_TO_RESULT(borrowedInsertStatement->Execute())); mJournalsToRemoveAfterCommit.AppendElement(aId); return Ok{}; }; QM_TRY(CollectEachInRange( mFileInfoEntries, [&update](const auto& entry) -> Result { const auto delta = entry.GetData()->Delta(); if (delta) { QM_TRY(update(entry.GetKey(), delta)); } return Ok{}; })); QM_TRY(MOZ_TO_RESULT(CreateJournals())); return NS_OK; } void DatabaseConnection::UpdateRefcountFunction::DidCommit() { MOZ_ASSERT(mConnection); mConnection->AssertIsOnConnectionThread(); AUTO_PROFILER_LABEL("DatabaseConnection::UpdateRefcountFunction::DidCommit", DOM); for (const auto& entry : mFileInfoEntries.Values()) { entry->MaybeUpdateDBRefs(); } QM_WARNONLY_TRY(QM_TO_RESULT(RemoveJournals(mJournalsToRemoveAfterCommit))); } void DatabaseConnection::UpdateRefcountFunction::DidAbort() { MOZ_ASSERT(mConnection); mConnection->AssertIsOnConnectionThread(); AUTO_PROFILER_LABEL("DatabaseConnection::UpdateRefcountFunction::DidAbort", DOM); QM_WARNONLY_TRY(QM_TO_RESULT(RemoveJournals(mJournalsToRemoveAfterAbort))); } void DatabaseConnection::UpdateRefcountFunction::StartSavepoint() { MOZ_ASSERT(mConnection); mConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(!mInSavepoint); MOZ_ASSERT(!mSavepointEntriesIndex.Count()); mInSavepoint = true; } void DatabaseConnection::UpdateRefcountFunction::ReleaseSavepoint() { MOZ_ASSERT(mConnection); mConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(mInSavepoint); mSavepointEntriesIndex.Clear(); mInSavepoint = false; } void DatabaseConnection::UpdateRefcountFunction::RollbackSavepoint() { MOZ_ASSERT(mConnection); mConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(mInSavepoint); for (const auto& entry : mSavepointEntriesIndex.Values()) { entry->DecBySavepointDelta(); } mInSavepoint = false; mSavepointEntriesIndex.Clear(); } void DatabaseConnection::UpdateRefcountFunction::Reset() { MOZ_ASSERT(mConnection); mConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(!mSavepointEntriesIndex.Count()); MOZ_ASSERT(!mInSavepoint); mJournalsToCreateBeforeCommit.Clear(); mJournalsToRemoveAfterCommit.Clear(); mJournalsToRemoveAfterAbort.Clear(); // DatabaseFileInfo implementation automatically removes unreferenced files, // but it's done asynchronously and with a delay. We want to remove them (and // decrease quota usage) before we fire the commit event. for (const auto& entry : mFileInfoEntries.Values()) { // We need to move mFileInfo into a raw pointer in order to release it // explicitly with aSyncDeleteFile == true. DatabaseFileInfo* const fileInfo = entry->ReleaseFileInfo().forget().take(); MOZ_ASSERT(fileInfo); fileInfo->Release(/* aSyncDeleteFile */ true); } mFileInfoEntries.Clear(); } nsresult DatabaseConnection::UpdateRefcountFunction::ProcessValue( mozIStorageValueArray* aValues, int32_t aIndex, UpdateType aUpdateType) { MOZ_ASSERT(mConnection); mConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(aValues); AUTO_PROFILER_LABEL( "DatabaseConnection::UpdateRefcountFunction::ProcessValue", DOM); QM_TRY_INSPECT(const int32_t& type, MOZ_TO_RESULT_INVOKE_MEMBER(aValues, GetTypeOfIndex, aIndex)); if (type == mozIStorageValueArray::VALUE_TYPE_NULL) { return NS_OK; } QM_TRY_INSPECT(const auto& ids, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( nsString, aValues, GetString, aIndex)); QM_TRY_INSPECT(const auto& files, DeserializeStructuredCloneFiles(mFileManager, ids)); for (const StructuredCloneFileParent& file : files) { const int64_t id = file.FileInfo().Id(); MOZ_ASSERT(id > 0); const auto entry = WrapNotNull(mFileInfoEntries.GetOrInsertNew(id, file.FileInfoPtr())); if (mInSavepoint) { mSavepointEntriesIndex.InsertOrUpdate(id, entry); } switch (aUpdateType) { case UpdateType::Increment: entry->IncDeltas(mInSavepoint); break; case UpdateType::Decrement: entry->DecDeltas(mInSavepoint); break; default: MOZ_CRASH("Unknown update type!"); } } return NS_OK; } nsresult DatabaseConnection::UpdateRefcountFunction::CreateJournals() { MOZ_ASSERT(mConnection); mConnection->AssertIsOnConnectionThread(); AUTO_PROFILER_LABEL( "DatabaseConnection::UpdateRefcountFunction::CreateJournals", DOM); const nsCOMPtr journalDirectory = mFileManager.GetJournalDirectory(); QM_TRY(OkIf(journalDirectory), NS_ERROR_FAILURE); for (const int64_t id : mJournalsToCreateBeforeCommit) { const nsCOMPtr file = DatabaseFileManager::GetFileForId(journalDirectory, id); QM_TRY(OkIf(file), NS_ERROR_FAILURE); QM_TRY(MOZ_TO_RESULT(file->Create(nsIFile::NORMAL_FILE_TYPE, 0644))); mJournalsToRemoveAfterAbort.AppendElement(id); } return NS_OK; } nsresult DatabaseConnection::UpdateRefcountFunction::RemoveJournals( const nsTArray& aJournals) { MOZ_ASSERT(mConnection); mConnection->AssertIsOnConnectionThread(); AUTO_PROFILER_LABEL( "DatabaseConnection::UpdateRefcountFunction::RemoveJournals", DOM); nsCOMPtr journalDirectory = mFileManager.GetJournalDirectory(); QM_TRY(OkIf(journalDirectory), NS_ERROR_FAILURE); for (const auto& journal : aJournals) { nsCOMPtr file = DatabaseFileManager::GetFileForId(journalDirectory, journal); QM_TRY(OkIf(file), NS_ERROR_FAILURE); QM_WARNONLY_TRY(QM_TO_RESULT(file->Remove(false))); } return NS_OK; } NS_IMPL_ISUPPORTS(DatabaseConnection::UpdateRefcountFunction, mozIStorageFunction) NS_IMETHODIMP DatabaseConnection::UpdateRefcountFunction::OnFunctionCall( mozIStorageValueArray* aValues, nsIVariant** _retval) { MOZ_ASSERT(aValues); MOZ_ASSERT(_retval); AUTO_PROFILER_LABEL( "DatabaseConnection::UpdateRefcountFunction::OnFunctionCall", DOM); #ifdef DEBUG { QM_TRY_INSPECT(const uint32_t& numEntries, MOZ_TO_RESULT_INVOKE_MEMBER(aValues, GetNumEntries), QM_ASSERT_UNREACHABLE); MOZ_ASSERT(numEntries == 2); QM_TRY_INSPECT(const int32_t& type1, MOZ_TO_RESULT_INVOKE_MEMBER(aValues, GetTypeOfIndex, 0), QM_ASSERT_UNREACHABLE); QM_TRY_INSPECT(const int32_t& type2, MOZ_TO_RESULT_INVOKE_MEMBER(aValues, GetTypeOfIndex, 1), QM_ASSERT_UNREACHABLE); MOZ_ASSERT(!(type1 == mozIStorageValueArray::VALUE_TYPE_NULL && type2 == mozIStorageValueArray::VALUE_TYPE_NULL)); } #endif QM_TRY(MOZ_TO_RESULT(ProcessValue(aValues, 0, UpdateType::Decrement))); QM_TRY(MOZ_TO_RESULT(ProcessValue(aValues, 1, UpdateType::Increment))); return NS_OK; } /******************************************************************************* * ConnectionPool implementation ******************************************************************************/ ConnectionPool::ConnectionPool() : mDatabasesMutex("ConnectionPool::mDatabasesMutex"), mIOTarget(MakeConnectionIOTarget()), mIdleTimer(NS_NewTimer()), mNextTransactionId(0), mTotalThreadCount(0) { AssertIsOnOwningThread(); AssertIsOnBackgroundThread(); MOZ_ASSERT(mIdleTimer); } ConnectionPool::~ConnectionPool() { AssertIsOnOwningThread(); MOZ_ASSERT(mIdleThreads.IsEmpty()); MOZ_ASSERT(mIdleDatabases.IsEmpty()); MOZ_ASSERT(!mIdleTimer); MOZ_ASSERT(mTargetIdleTime.IsNull()); MOZ_ASSERT(!mDatabases.Count()); MOZ_ASSERT(!mTransactions.Count()); MOZ_ASSERT(mQueuedTransactions.IsEmpty()); MOZ_ASSERT(mCompleteCallbacks.IsEmpty()); MOZ_ASSERT(!mTotalThreadCount); MOZ_ASSERT(mShutdownRequested); MOZ_ASSERT(mShutdownComplete); } // static void ConnectionPool::IdleTimerCallback(nsITimer* aTimer, void* aClosure) { MOZ_ASSERT(aTimer); MOZ_ASSERT(aClosure); AUTO_PROFILER_LABEL("ConnectionPool::IdleTimerCallback", DOM); auto& self = *static_cast(aClosure); MOZ_ASSERT(self.mIdleTimer); MOZ_ASSERT(SameCOMIdentity(self.mIdleTimer, aTimer)); MOZ_ASSERT(!self.mTargetIdleTime.IsNull()); MOZ_ASSERT_IF(self.mIdleDatabases.IsEmpty(), !self.mIdleThreads.IsEmpty()); MOZ_ASSERT_IF(self.mIdleThreads.IsEmpty(), !self.mIdleDatabases.IsEmpty()); self.mTargetIdleTime = TimeStamp(); // Cheat a little. const TimeStamp now = TimeStamp::NowLoRes() + TimeDuration::FromMilliseconds(500); // XXX Move this to ArrayAlgorithm.h? const auto removeUntil = [](auto& array, auto&& cond) { const auto begin = array.begin(), end = array.end(); array.RemoveElementsRange( begin, std::find_if(begin, end, std::forward(cond))); }; removeUntil(self.mIdleDatabases, [now, &self](const auto& info) { if (now >= info.mIdleTime) { if ((*info.mDatabaseInfo)->mIdle) { self.PerformIdleDatabaseMaintenance(*info.mDatabaseInfo.ref()); } else { self.CloseDatabase(*info.mDatabaseInfo.ref()); } return false; } return true; }); removeUntil(self.mIdleThreads, [now, &self](auto& info) { info.mThreadInfo.AssertValid(); if (now >= info.mIdleTime) { self.ShutdownThread(std::move(info.mThreadInfo)); return false; } return true; }); self.AdjustIdleTimer(); } Result, nsresult> ConnectionPool::GetOrCreateConnection(const Database& aDatabase) { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!IsOnBackgroundThread()); AUTO_PROFILER_LABEL("ConnectionPool::GetOrCreateConnection", DOM); DatabaseInfo* dbInfo; { MutexAutoLock lock(mDatabasesMutex); dbInfo = mDatabases.Get(aDatabase.Id()); } MOZ_ASSERT(dbInfo); if (dbInfo->mConnection) { dbInfo->AssertIsOnConnectionThread(); return dbInfo->mConnection; } MOZ_ASSERT(!dbInfo->mDEBUGConnectionEventTarget); QM_TRY_UNWRAP( MovingNotNull> storageConnection, GetStorageConnection(aDatabase.FilePath(), aDatabase.DirectoryLockId(), aDatabase.TelemetryId(), aDatabase.MaybeKeyRef())); RefPtr connection = new DatabaseConnection( std::move(storageConnection), aDatabase.GetFileManagerPtr()); QM_TRY(MOZ_TO_RESULT(connection->Init())); dbInfo->mConnection = connection; IDB_DEBUG_LOG(("ConnectionPool created connection 0x%p for '%s'", dbInfo->mConnection.get(), NS_ConvertUTF16toUTF8(aDatabase.FilePath()).get())); #ifdef DEBUG dbInfo->mDEBUGConnectionEventTarget = GetCurrentSerialEventTarget(); #endif return connection; } uint64_t ConnectionPool::Start( const nsID& aBackgroundChildLoggingId, const nsACString& aDatabaseId, int64_t aLoggingSerialNumber, const nsTArray& aObjectStoreNames, bool aIsWriteTransaction, TransactionDatabaseOperationBase* aTransactionOp) { AssertIsOnOwningThread(); MOZ_ASSERT(!aDatabaseId.IsEmpty()); MOZ_ASSERT(mNextTransactionId < UINT64_MAX); MOZ_ASSERT(!mShutdownRequested); AUTO_PROFILER_LABEL("ConnectionPool::Start", DOM); const uint64_t transactionId = ++mNextTransactionId; // To avoid always acquiring a lock, we don't use WithEntryHandle here, which // would require a lock in any case. DatabaseInfo* dbInfo = mDatabases.Get(aDatabaseId); const bool databaseInfoIsNew = !dbInfo; if (databaseInfoIsNew) { MutexAutoLock lock(mDatabasesMutex); dbInfo = mDatabases .InsertOrUpdate(aDatabaseId, MakeUnique(this, aDatabaseId)) .get(); } MOZ_ASSERT(!mTransactions.Contains(transactionId)); auto& transactionInfo = *mTransactions.InsertOrUpdate( transactionId, MakeUnique( *dbInfo, aBackgroundChildLoggingId, aDatabaseId, transactionId, aLoggingSerialNumber, aObjectStoreNames, aIsWriteTransaction, aTransactionOp)); if (aIsWriteTransaction) { MOZ_ASSERT(dbInfo->mWriteTransactionCount < UINT32_MAX); dbInfo->mWriteTransactionCount++; } else { MOZ_ASSERT(dbInfo->mReadTransactionCount < UINT32_MAX); dbInfo->mReadTransactionCount++; } auto& blockingTransactions = dbInfo->mBlockingTransactions; for (const nsAString& objectStoreName : aObjectStoreNames) { TransactionInfoPair* blockInfo = blockingTransactions.GetOrInsertNew(objectStoreName); // Mark what we are blocking on. if (const auto maybeBlockingRead = blockInfo->mLastBlockingReads) { transactionInfo.mBlockedOn.Insert(&maybeBlockingRead.ref()); maybeBlockingRead->AddBlockingTransaction(transactionInfo); } if (aIsWriteTransaction) { for (const auto blockingWrite : blockInfo->mLastBlockingWrites) { transactionInfo.mBlockedOn.Insert(blockingWrite); blockingWrite->AddBlockingTransaction(transactionInfo); } blockInfo->mLastBlockingReads = SomeRef(transactionInfo); blockInfo->mLastBlockingWrites.Clear(); } else { blockInfo->mLastBlockingWrites.AppendElement( WrapNotNullUnchecked(&transactionInfo)); } } if (!transactionInfo.mBlockedOn.Count()) { Unused << ScheduleTransaction(transactionInfo, /* aFromQueuedTransactions */ false); } if (!databaseInfoIsNew && (mIdleDatabases.RemoveElement(dbInfo) || mDatabasesPerformingIdleMaintenance.RemoveElement(dbInfo))) { AdjustIdleTimer(); } return transactionId; } void ConnectionPool::Dispatch(uint64_t aTransactionId, nsIRunnable* aRunnable) { AssertIsOnOwningThread(); MOZ_ASSERT(aRunnable); AUTO_PROFILER_LABEL("ConnectionPool::Dispatch", DOM); auto* const transactionInfo = mTransactions.Get(aTransactionId); MOZ_ASSERT(transactionInfo); MOZ_ASSERT(!transactionInfo->mFinished); if (transactionInfo->mRunning) { DatabaseInfo& dbInfo = transactionInfo->mDatabaseInfo; dbInfo.mThreadInfo.AssertValid(); MOZ_ASSERT(!dbInfo.mClosing); MOZ_ASSERT_IF( transactionInfo->mIsWriteTransaction, dbInfo.mRunningWriteTransaction && dbInfo.mRunningWriteTransaction.refEquals(*transactionInfo)); MOZ_ALWAYS_SUCCEEDS( dbInfo.mThreadInfo.ThreadRef().Dispatch(aRunnable, NS_DISPATCH_NORMAL)); } else { transactionInfo->mQueuedRunnables.AppendElement(aRunnable); } } void ConnectionPool::Finish(uint64_t aTransactionId, FinishCallback* aCallback) { AssertIsOnOwningThread(); #ifdef DEBUG auto* const transactionInfo = mTransactions.Get(aTransactionId); MOZ_ASSERT(transactionInfo); MOZ_ASSERT(!transactionInfo->mFinished); #endif AUTO_PROFILER_LABEL("ConnectionPool::Finish", DOM); RefPtr wrapper = new FinishCallbackWrapper(this, aTransactionId, aCallback); Dispatch(aTransactionId, wrapper); #ifdef DEBUG transactionInfo->mFinished.Flip(); #endif } void ConnectionPool::WaitForDatabaseToComplete(const nsCString& aDatabaseId, nsIRunnable* aCallback) { AssertIsOnOwningThread(); MOZ_ASSERT(!aDatabaseId.IsEmpty()); MOZ_ASSERT(aCallback); AUTO_PROFILER_LABEL("ConnectionPool::WaitForDatabaseToComplete", DOM); if (!CloseDatabaseWhenIdleInternal(aDatabaseId)) { Unused << aCallback->Run(); return; } mCompleteCallbacks.EmplaceBack( MakeUnique(aDatabaseId, aCallback)); } void ConnectionPool::Shutdown() { AssertIsOnOwningThread(); MOZ_ASSERT(!mShutdownComplete); AUTO_PROFILER_LABEL("ConnectionPool::Shutdown", DOM); mShutdownRequested.Flip(); CancelIdleTimer(); MOZ_ASSERT(mTargetIdleTime.IsNull()); mIdleTimer = nullptr; CloseIdleDatabases(); ShutdownIdleThreads(); if (!mDatabases.Count()) { MOZ_ASSERT(!mTransactions.Count()); Cleanup(); MOZ_ASSERT(mShutdownComplete); return; } MOZ_ALWAYS_TRUE(SpinEventLoopUntil("ConnectionPool::Shutdown"_ns, [&]() { return static_cast(mShutdownComplete); })); } void ConnectionPool::Cleanup() { AssertIsOnOwningThread(); MOZ_ASSERT(mShutdownRequested); MOZ_ASSERT(!mShutdownComplete); MOZ_ASSERT(!mDatabases.Count()); MOZ_ASSERT(!mTransactions.Count()); MOZ_ASSERT(mIdleThreads.IsEmpty()); AUTO_PROFILER_LABEL("ConnectionPool::Cleanup", DOM); if (!mCompleteCallbacks.IsEmpty()) { // Run all callbacks manually now. { auto completeCallbacks = std::move(mCompleteCallbacks); for (const auto& completeCallback : completeCallbacks) { MOZ_ASSERT(completeCallback); MOZ_ASSERT(completeCallback->mCallback); Unused << completeCallback->mCallback->Run(); } // We expect no new callbacks being completed by running the existing // ones. MOZ_ASSERT(mCompleteCallbacks.IsEmpty()); } // And make sure they get processed. nsIThread* currentThread = NS_GetCurrentThread(); MOZ_ASSERT(currentThread); MOZ_ALWAYS_SUCCEEDS(NS_ProcessPendingEvents(currentThread)); } mShutdownComplete.Flip(); } void ConnectionPool::AdjustIdleTimer() { AssertIsOnOwningThread(); MOZ_ASSERT(mIdleTimer); AUTO_PROFILER_LABEL("ConnectionPool::AdjustIdleTimer", DOM); // Figure out the next time at which we should release idle resources. This // includes both databases and threads. TimeStamp newTargetIdleTime; MOZ_ASSERT(newTargetIdleTime.IsNull()); if (!mIdleDatabases.IsEmpty()) { newTargetIdleTime = mIdleDatabases[0].mIdleTime; } if (!mIdleThreads.IsEmpty()) { const TimeStamp& idleTime = mIdleThreads[0].mIdleTime; if (newTargetIdleTime.IsNull() || idleTime < newTargetIdleTime) { newTargetIdleTime = idleTime; } } MOZ_ASSERT_IF(newTargetIdleTime.IsNull(), mIdleDatabases.IsEmpty()); MOZ_ASSERT_IF(newTargetIdleTime.IsNull(), mIdleThreads.IsEmpty()); // Cancel the timer if it was running and the new target time is different. if (!mTargetIdleTime.IsNull() && (newTargetIdleTime.IsNull() || mTargetIdleTime != newTargetIdleTime)) { CancelIdleTimer(); MOZ_ASSERT(mTargetIdleTime.IsNull()); } // Schedule the timer if we have a target time different than before. if (!newTargetIdleTime.IsNull() && (mTargetIdleTime.IsNull() || mTargetIdleTime != newTargetIdleTime)) { double delta = (newTargetIdleTime - TimeStamp::NowLoRes()).ToMilliseconds(); uint32_t delay; if (delta > 0) { delay = uint32_t(std::min(delta, double(UINT32_MAX))); } else { delay = 0; } MOZ_ALWAYS_SUCCEEDS(mIdleTimer->InitWithNamedFuncCallback( IdleTimerCallback, this, delay, nsITimer::TYPE_ONE_SHOT, "ConnectionPool::IdleTimerCallback")); mTargetIdleTime = newTargetIdleTime; } } void ConnectionPool::CancelIdleTimer() { AssertIsOnOwningThread(); MOZ_ASSERT(mIdleTimer); if (!mTargetIdleTime.IsNull()) { MOZ_ALWAYS_SUCCEEDS(mIdleTimer->Cancel()); mTargetIdleTime = TimeStamp(); MOZ_ASSERT(mTargetIdleTime.IsNull()); } } void ConnectionPool::ShutdownThread(ThreadInfo aThreadInfo) { AssertIsOnOwningThread(); MOZ_ASSERT(mTotalThreadCount); // We need to move thread and runnable separately. auto [thread, runnable] = aThreadInfo.Forget(); IDB_DEBUG_LOG(("ConnectionPool shutting down thread %" PRIu32, runnable->SerialNumber())); // This should clean up the thread with the profiler. MOZ_ALWAYS_SUCCEEDS(thread->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL)); MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(NewRunnableMethod( "nsIThread::AsyncShutdown", thread, &nsIThread::AsyncShutdown))); mTotalThreadCount--; } void ConnectionPool::CloseIdleDatabases() { AssertIsOnOwningThread(); MOZ_ASSERT(mShutdownRequested); AUTO_PROFILER_LABEL("ConnectionPool::CloseIdleDatabases", DOM); if (!mIdleDatabases.IsEmpty()) { for (IdleDatabaseInfo& idleInfo : mIdleDatabases) { CloseDatabase(*idleInfo.mDatabaseInfo.ref()); } mIdleDatabases.Clear(); } if (!mDatabasesPerformingIdleMaintenance.IsEmpty()) { for (const auto dbInfo : mDatabasesPerformingIdleMaintenance) { CloseDatabase(*dbInfo); } mDatabasesPerformingIdleMaintenance.Clear(); } } void ConnectionPool::ShutdownIdleThreads() { AssertIsOnOwningThread(); MOZ_ASSERT(mShutdownRequested); AUTO_PROFILER_LABEL("ConnectionPool::ShutdownIdleThreads", DOM); for (auto& idleThread : mIdleThreads) { ShutdownThread(std::move(idleThread.mThreadInfo)); } mIdleThreads.Clear(); } bool ConnectionPool::ScheduleTransaction(TransactionInfo& aTransactionInfo, bool aFromQueuedTransactions) { AssertIsOnOwningThread(); AUTO_PROFILER_LABEL("ConnectionPool::ScheduleTransaction", DOM); DatabaseInfo& dbInfo = aTransactionInfo.mDatabaseInfo; dbInfo.mIdle = false; if (dbInfo.mClosing) { MOZ_ASSERT(!mIdleDatabases.Contains(&dbInfo)); MOZ_ASSERT( !dbInfo.mTransactionsScheduledDuringClose.Contains(&aTransactionInfo)); dbInfo.mTransactionsScheduledDuringClose.AppendElement( WrapNotNullUnchecked(&aTransactionInfo)); return true; } if (!dbInfo.mThreadInfo.IsValid()) { if (mIdleThreads.IsEmpty()) { bool created = false; if (mTotalThreadCount < kMaxConnectionThreadCount) { const uint32_t serialNumber = SerialNumber(); const nsCString serialName = nsPrintfCString("IndexedDB #%" PRIu32, serialNumber); // This will set the thread up with the profiler. RefPtr runnable = new ThreadRunnable(serialNumber); nsCOMPtr newThread; nsresult rv = NS_NewNamedThread(serialName, getter_AddRefs(newThread), runnable); if (NS_SUCCEEDED(rv)) { newThread->SetNameForWakeupTelemetry("IndexedDB (all)"_ns); MOZ_ASSERT(newThread); IDB_DEBUG_LOG(("ConnectionPool created thread %" PRIu32, runnable->SerialNumber())); dbInfo.mThreadInfo = ThreadInfo{std::move(newThread), std::move(runnable)}; mTotalThreadCount++; created = true; } else { NS_WARNING("Failed to make new thread!"); } } else if (!mDatabasesPerformingIdleMaintenance.IsEmpty()) { // We need a thread right now so force all idle processing to stop by // posting a dummy runnable to each thread that might be doing idle // maintenance. // // This is copied for each database inside the loop below, it is // deliberately const to prevent the attempt to wrongly optimize the // refcounting by passing runnable.forget() to the Dispatch method, see // bug 1598559. const nsCOMPtr runnable = new Runnable("IndexedDBDummyRunnable"); for (uint32_t index = mDatabasesPerformingIdleMaintenance.Length(); index > 0; index--) { const auto dbInfo = mDatabasesPerformingIdleMaintenance[index - 1]; dbInfo->mThreadInfo.AssertValid(); MOZ_ALWAYS_SUCCEEDS(dbInfo->mThreadInfo.ThreadRef().Dispatch( runnable, NS_DISPATCH_NORMAL)); } } if (!created) { if (!aFromQueuedTransactions) { MOZ_ASSERT(!mQueuedTransactions.Contains(&aTransactionInfo)); mQueuedTransactions.AppendElement( WrapNotNullUnchecked(&aTransactionInfo)); } return false; } } else { dbInfo.mThreadInfo = std::move(mIdleThreads.PopLastElement().mThreadInfo); AdjustIdleTimer(); } } dbInfo.mThreadInfo.AssertValid(); if (aTransactionInfo.mIsWriteTransaction) { if (dbInfo.mRunningWriteTransaction) { // SQLite only allows one write transaction at a time so queue this // transaction for later. MOZ_ASSERT( !dbInfo.mScheduledWriteTransactions.Contains(&aTransactionInfo)); dbInfo.mScheduledWriteTransactions.AppendElement( WrapNotNullUnchecked(&aTransactionInfo)); return true; } dbInfo.mRunningWriteTransaction = SomeRef(aTransactionInfo); dbInfo.mNeedsCheckpoint = true; } MOZ_ASSERT(!aTransactionInfo.mRunning); aTransactionInfo.mRunning = true; nsTArray>& queuedRunnables = aTransactionInfo.mQueuedRunnables; if (!queuedRunnables.IsEmpty()) { for (auto& queuedRunnable : queuedRunnables) { MOZ_ALWAYS_SUCCEEDS(dbInfo.mThreadInfo.ThreadRef().Dispatch( queuedRunnable.forget(), NS_DISPATCH_NORMAL)); } queuedRunnables.Clear(); } return true; } void ConnectionPool::NoteFinishedTransaction(uint64_t aTransactionId) { AssertIsOnOwningThread(); AUTO_PROFILER_LABEL("ConnectionPool::NoteFinishedTransaction", DOM); auto* const transactionInfo = mTransactions.Get(aTransactionId); MOZ_ASSERT(transactionInfo); MOZ_ASSERT(transactionInfo->mRunning); MOZ_ASSERT(transactionInfo->mFinished); transactionInfo->mRunning = false; DatabaseInfo& dbInfo = transactionInfo->mDatabaseInfo; MOZ_ASSERT(mDatabases.Get(transactionInfo->mDatabaseId) == &dbInfo); dbInfo.mThreadInfo.AssertValid(); // Schedule the next write transaction if there are any queued. if (dbInfo.mRunningWriteTransaction && dbInfo.mRunningWriteTransaction.refEquals(*transactionInfo)) { MOZ_ASSERT(transactionInfo->mIsWriteTransaction); MOZ_ASSERT(dbInfo.mNeedsCheckpoint); dbInfo.mRunningWriteTransaction = Nothing(); if (!dbInfo.mScheduledWriteTransactions.IsEmpty()) { const auto nextWriteTransaction = dbInfo.mScheduledWriteTransactions[0]; dbInfo.mScheduledWriteTransactions.RemoveElementAt(0); MOZ_ALWAYS_TRUE(ScheduleTransaction(*nextWriteTransaction, /* aFromQueuedTransactions */ false)); } } for (const auto& objectStoreName : transactionInfo->mObjectStoreNames) { TransactionInfoPair* blockInfo = dbInfo.mBlockingTransactions.Get(objectStoreName); MOZ_ASSERT(blockInfo); if (transactionInfo->mIsWriteTransaction && blockInfo->mLastBlockingReads && blockInfo->mLastBlockingReads.refEquals(*transactionInfo)) { blockInfo->mLastBlockingReads = Nothing(); } blockInfo->mLastBlockingWrites.RemoveElement(transactionInfo); } transactionInfo->RemoveBlockingTransactions(); if (transactionInfo->mIsWriteTransaction) { MOZ_ASSERT(dbInfo.mWriteTransactionCount); dbInfo.mWriteTransactionCount--; } else { MOZ_ASSERT(dbInfo.mReadTransactionCount); dbInfo.mReadTransactionCount--; } mTransactions.Remove(aTransactionId); if (!dbInfo.TotalTransactionCount()) { MOZ_ASSERT(!dbInfo.mIdle); dbInfo.mIdle = true; NoteIdleDatabase(dbInfo); } } void ConnectionPool::ScheduleQueuedTransactions(ThreadInfo aThreadInfo) { AssertIsOnOwningThread(); aThreadInfo.AssertValid(); MOZ_ASSERT(!mQueuedTransactions.IsEmpty()); AUTO_PROFILER_LABEL("ConnectionPool::ScheduleQueuedTransactions", DOM); auto idleThreadInfo = IdleThreadInfo{std::move(aThreadInfo)}; MOZ_ASSERT(!mIdleThreads.Contains(idleThreadInfo)); mIdleThreads.InsertElementSorted(std::move(idleThreadInfo)); const auto foundIt = std::find_if( mQueuedTransactions.begin(), mQueuedTransactions.end(), [&me = *this](const auto& queuedTransaction) { return !me.ScheduleTransaction(*queuedTransaction, /* aFromQueuedTransactions */ true); }); mQueuedTransactions.RemoveElementsRange(mQueuedTransactions.begin(), foundIt); AdjustIdleTimer(); } void ConnectionPool::NoteIdleDatabase(DatabaseInfo& aDatabaseInfo) { AssertIsOnOwningThread(); MOZ_ASSERT(!aDatabaseInfo.TotalTransactionCount()); aDatabaseInfo.mThreadInfo.AssertValid(); MOZ_ASSERT(!mIdleDatabases.Contains(&aDatabaseInfo)); AUTO_PROFILER_LABEL("ConnectionPool::NoteIdleDatabase", DOM); const bool otherDatabasesWaiting = !mQueuedTransactions.IsEmpty(); if (mShutdownRequested || otherDatabasesWaiting || aDatabaseInfo.mCloseOnIdle) { // Make sure we close the connection if we're shutting down or giving the // thread to another database. CloseDatabase(aDatabaseInfo); if (otherDatabasesWaiting) { // Let another database use this thread. ScheduleQueuedTransactions(std::move(aDatabaseInfo.mThreadInfo)); } else if (mShutdownRequested) { // If there are no other databases that need to run then we can shut this // thread down immediately instead of going through the idle thread // mechanism. ShutdownThread(std::move(aDatabaseInfo.mThreadInfo)); } return; } mIdleDatabases.InsertElementSorted(IdleDatabaseInfo{aDatabaseInfo}); AdjustIdleTimer(); } void ConnectionPool::NoteClosedDatabase(DatabaseInfo& aDatabaseInfo) { AssertIsOnOwningThread(); MOZ_ASSERT(aDatabaseInfo.mClosing); MOZ_ASSERT(!mIdleDatabases.Contains(&aDatabaseInfo)); AUTO_PROFILER_LABEL("ConnectionPool::NoteClosedDatabase", DOM); aDatabaseInfo.mClosing = false; // Figure out what to do with this database's thread. It may have already been // given to another database, in which case there's nothing to do here. // Otherwise we prioritize the thread as follows: // 1. Databases that haven't had an opportunity to run at all are highest // priority. Those live in the |mQueuedTransactions| list. // 2. If this database has additional transactions that were started after // we began closing the connection then the thread can be reused for // those transactions. // 3. If we're shutting down then we can get rid of the thread. // 4. Finally, if nothing above took the thread then we can add it to our // list of idle threads. It may be reused or it may time out. If we have // too many idle threads then we will shut down the oldest. if (aDatabaseInfo.mThreadInfo.IsValid()) { if (!mQueuedTransactions.IsEmpty()) { // Give the thread to another database. ScheduleQueuedTransactions(std::move(aDatabaseInfo.mThreadInfo)); } else if (!aDatabaseInfo.TotalTransactionCount()) { if (mShutdownRequested) { ShutdownThread(std::move(aDatabaseInfo.mThreadInfo)); } else { auto idleThreadInfo = IdleThreadInfo{std::move(aDatabaseInfo.mThreadInfo)}; MOZ_ASSERT(!mIdleThreads.Contains(idleThreadInfo)); mIdleThreads.InsertElementSorted(std::move(idleThreadInfo)); if (mIdleThreads.Length() > kMaxIdleConnectionThreadCount) { ShutdownThread(std::move(mIdleThreads[0].mThreadInfo)); mIdleThreads.RemoveElementAt(0); } AdjustIdleTimer(); } } } // Schedule any transactions that were started while we were closing the // connection. if (aDatabaseInfo.TotalTransactionCount()) { auto& scheduledTransactions = aDatabaseInfo.mTransactionsScheduledDuringClose; MOZ_ASSERT(!scheduledTransactions.IsEmpty()); for (const auto& scheduledTransaction : scheduledTransactions) { Unused << ScheduleTransaction(*scheduledTransaction, /* aFromQueuedTransactions */ false); } scheduledTransactions.Clear(); return; } // There are no more transactions and the connection has been closed. We're // done with this database. { MutexAutoLock lock(mDatabasesMutex); mDatabases.Remove(aDatabaseInfo.mDatabaseId); } // That just deleted |aDatabaseInfo|, we must not access that below. // See if we need to fire any complete callbacks now that the database is // finished. mCompleteCallbacks.RemoveLastElements( mCompleteCallbacks.end() - std::remove_if(mCompleteCallbacks.begin(), mCompleteCallbacks.end(), [&me = *this](const auto& completeCallback) { return me.MaybeFireCallback(completeCallback.get()); })); // If that was the last database and we're supposed to be shutting down then // we are finished. if (mShutdownRequested && !mDatabases.Count()) { MOZ_ASSERT(!mTransactions.Count()); Cleanup(); } } bool ConnectionPool::MaybeFireCallback(DatabaseCompleteCallback* aCallback) { AssertIsOnOwningThread(); MOZ_ASSERT(aCallback); MOZ_ASSERT(!aCallback->mDatabaseId.IsEmpty()); MOZ_ASSERT(aCallback->mCallback); AUTO_PROFILER_LABEL("ConnectionPool::MaybeFireCallback", DOM); if (mDatabases.Get(aCallback->mDatabaseId)) { return false; } Unused << aCallback->mCallback->Run(); return true; } void ConnectionPool::PerformIdleDatabaseMaintenance( DatabaseInfo& aDatabaseInfo) { AssertIsOnOwningThread(); MOZ_ASSERT(!aDatabaseInfo.TotalTransactionCount()); aDatabaseInfo.mThreadInfo.AssertValid(); MOZ_ASSERT(aDatabaseInfo.mIdle); MOZ_ASSERT(!aDatabaseInfo.mCloseOnIdle); MOZ_ASSERT(!aDatabaseInfo.mClosing); MOZ_ASSERT(mIdleDatabases.Contains(&aDatabaseInfo)); MOZ_ASSERT(!mDatabasesPerformingIdleMaintenance.Contains(&aDatabaseInfo)); const bool neededCheckpoint = aDatabaseInfo.mNeedsCheckpoint; aDatabaseInfo.mNeedsCheckpoint = false; aDatabaseInfo.mIdle = false; mDatabasesPerformingIdleMaintenance.AppendElement( WrapNotNullUnchecked(&aDatabaseInfo)); MOZ_ALWAYS_SUCCEEDS(aDatabaseInfo.mThreadInfo.ThreadRef().Dispatch( MakeAndAddRef(aDatabaseInfo, neededCheckpoint), NS_DISPATCH_NORMAL)); } void ConnectionPool::CloseDatabase(DatabaseInfo& aDatabaseInfo) const { AssertIsOnOwningThread(); MOZ_DIAGNOSTIC_ASSERT(!aDatabaseInfo.TotalTransactionCount()); aDatabaseInfo.mThreadInfo.AssertValid(); MOZ_ASSERT(!aDatabaseInfo.mClosing); aDatabaseInfo.mIdle = false; aDatabaseInfo.mNeedsCheckpoint = false; aDatabaseInfo.mClosing = true; MOZ_ALWAYS_SUCCEEDS(aDatabaseInfo.mThreadInfo.ThreadRef().Dispatch( MakeAndAddRef(aDatabaseInfo), NS_DISPATCH_NORMAL)); } bool ConnectionPool::CloseDatabaseWhenIdleInternal( const nsACString& aDatabaseId) { AssertIsOnOwningThread(); MOZ_ASSERT(!aDatabaseId.IsEmpty()); AUTO_PROFILER_LABEL("ConnectionPool::CloseDatabaseWhenIdleInternal", DOM); if (DatabaseInfo* dbInfo = mDatabases.Get(aDatabaseId)) { if (mIdleDatabases.RemoveElement(dbInfo) || mDatabasesPerformingIdleMaintenance.RemoveElement(dbInfo)) { CloseDatabase(*dbInfo); AdjustIdleTimer(); } else { dbInfo->mCloseOnIdle.EnsureFlipped(); } return true; } return false; } ConnectionPool::ConnectionRunnable::ConnectionRunnable( DatabaseInfo& aDatabaseInfo) : Runnable("dom::indexedDB::ConnectionPool::ConnectionRunnable"), mDatabaseInfo(aDatabaseInfo), mOwningEventTarget(GetCurrentSerialEventTarget()) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aDatabaseInfo.mConnectionPool); aDatabaseInfo.mConnectionPool->AssertIsOnOwningThread(); MOZ_ASSERT(mOwningEventTarget); } NS_IMETHODIMP ConnectionPool::IdleConnectionRunnable::Run() { MOZ_ASSERT(!mDatabaseInfo.mIdle); const nsCOMPtr owningThread = std::move(mOwningEventTarget); if (owningThread) { mDatabaseInfo.AssertIsOnConnectionThread(); // The connection could be null if EnsureConnection() didn't run or was not // successful in TransactionDatabaseOperationBase::RunOnConnectionThread(). if (mDatabaseInfo.mConnection) { mDatabaseInfo.mConnection->DoIdleProcessing(mNeedsCheckpoint); } MOZ_ALWAYS_SUCCEEDS(owningThread->Dispatch(this, NS_DISPATCH_NORMAL)); return NS_OK; } AssertIsOnBackgroundThread(); RefPtr connectionPool = mDatabaseInfo.mConnectionPool; MOZ_ASSERT(connectionPool); if (mDatabaseInfo.mClosing || mDatabaseInfo.TotalTransactionCount()) { MOZ_ASSERT(!connectionPool->mDatabasesPerformingIdleMaintenance.Contains( &mDatabaseInfo)); } else { MOZ_ALWAYS_TRUE( connectionPool->mDatabasesPerformingIdleMaintenance.RemoveElement( &mDatabaseInfo)); connectionPool->NoteIdleDatabase(mDatabaseInfo); } return NS_OK; } NS_IMETHODIMP ConnectionPool::CloseConnectionRunnable::Run() { AUTO_PROFILER_LABEL("ConnectionPool::CloseConnectionRunnable::Run", DOM); if (mOwningEventTarget) { MOZ_ASSERT(mDatabaseInfo.mClosing); const nsCOMPtr owningThread = std::move(mOwningEventTarget); // The connection could be null if EnsureConnection() didn't run or was not // successful in TransactionDatabaseOperationBase::RunOnConnectionThread(). if (mDatabaseInfo.mConnection) { mDatabaseInfo.AssertIsOnConnectionThread(); mDatabaseInfo.mConnection->Close(); IDB_DEBUG_LOG(("ConnectionPool closed connection 0x%p", mDatabaseInfo.mConnection.get())); mDatabaseInfo.mConnection = nullptr; #ifdef DEBUG mDatabaseInfo.mDEBUGConnectionEventTarget = nullptr; #endif } MOZ_ALWAYS_SUCCEEDS(owningThread->Dispatch(this, NS_DISPATCH_NORMAL)); return NS_OK; } RefPtr connectionPool = mDatabaseInfo.mConnectionPool; MOZ_ASSERT(connectionPool); connectionPool->NoteClosedDatabase(mDatabaseInfo); return NS_OK; } ConnectionPool::DatabaseInfo::DatabaseInfo(ConnectionPool* aConnectionPool, const nsACString& aDatabaseId) : mConnectionPool(aConnectionPool), mDatabaseId(aDatabaseId), mReadTransactionCount(0), mWriteTransactionCount(0), mNeedsCheckpoint(false), mIdle(false), mClosing(false) #ifdef DEBUG , mDEBUGConnectionEventTarget(nullptr) #endif { AssertIsOnBackgroundThread(); MOZ_ASSERT(aConnectionPool); aConnectionPool->AssertIsOnOwningThread(); MOZ_ASSERT(!aDatabaseId.IsEmpty()); MOZ_COUNT_CTOR(ConnectionPool::DatabaseInfo); } ConnectionPool::DatabaseInfo::~DatabaseInfo() { AssertIsOnBackgroundThread(); MOZ_ASSERT(!mConnection); MOZ_ASSERT(mScheduledWriteTransactions.IsEmpty()); MOZ_ASSERT(!mRunningWriteTransaction); mThreadInfo.AssertEmpty(); MOZ_ASSERT(!TotalTransactionCount()); MOZ_COUNT_DTOR(ConnectionPool::DatabaseInfo); } ConnectionPool::DatabaseCompleteCallback::DatabaseCompleteCallback( const nsCString& aDatabaseId, nsIRunnable* aCallback) : mDatabaseId(aDatabaseId), mCallback(aCallback) { AssertIsOnBackgroundThread(); MOZ_ASSERT(!mDatabaseId.IsEmpty()); MOZ_ASSERT(aCallback); MOZ_COUNT_CTOR(ConnectionPool::DatabaseCompleteCallback); } ConnectionPool::DatabaseCompleteCallback::~DatabaseCompleteCallback() { AssertIsOnBackgroundThread(); MOZ_COUNT_DTOR(ConnectionPool::DatabaseCompleteCallback); } ConnectionPool::FinishCallbackWrapper::FinishCallbackWrapper( ConnectionPool* aConnectionPool, uint64_t aTransactionId, FinishCallback* aCallback) : Runnable("dom::indexedDB::ConnectionPool::FinishCallbackWrapper"), mConnectionPool(aConnectionPool), mCallback(aCallback), mOwningEventTarget(GetCurrentSerialEventTarget()), mTransactionId(aTransactionId), mHasRunOnce(false) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aConnectionPool); MOZ_ASSERT(aCallback); MOZ_ASSERT(mOwningEventTarget); } ConnectionPool::FinishCallbackWrapper::~FinishCallbackWrapper() { MOZ_ASSERT(!mConnectionPool); MOZ_ASSERT(!mCallback); } nsresult ConnectionPool::FinishCallbackWrapper::Run() { MOZ_ASSERT(mConnectionPool); MOZ_ASSERT(mCallback); MOZ_ASSERT(mOwningEventTarget); AUTO_PROFILER_LABEL("ConnectionPool::FinishCallbackWrapper::Run", DOM); if (!mHasRunOnce) { MOZ_ASSERT(!IsOnBackgroundThread()); mHasRunOnce = true; Unused << mCallback->Run(); MOZ_ALWAYS_SUCCEEDS(mOwningEventTarget->Dispatch(this, NS_DISPATCH_NORMAL)); return NS_OK; } mConnectionPool->AssertIsOnOwningThread(); MOZ_ASSERT(mHasRunOnce); RefPtr connectionPool = std::move(mConnectionPool); RefPtr callback = std::move(mCallback); callback->TransactionFinishedBeforeUnblock(); connectionPool->NoteFinishedTransaction(mTransactionId); callback->TransactionFinishedAfterUnblock(); return NS_OK; } uint32_t ConnectionPool::sSerialNumber = 0u; ConnectionPool::ThreadRunnable::ThreadRunnable(uint32_t aSerialNumber) : Runnable("dom::indexedDB::ConnectionPool::ThreadRunnable"), mSerialNumber(aSerialNumber) { AssertIsOnBackgroundThread(); } ConnectionPool::ThreadRunnable::~ThreadRunnable() { MOZ_ASSERT(!mFirstRun); MOZ_ASSERT(!mContinueRunning); } nsresult ConnectionPool::ThreadRunnable::Run() { MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(mContinueRunning); if (!mFirstRun) { mContinueRunning.Flip(); return NS_OK; } mFirstRun.Flip(); { // Scope for the profiler label. AUTO_PROFILER_LABEL("ConnectionPool::ThreadRunnable::Run", DOM); DebugOnly currentThread = NS_GetCurrentThread(); MOZ_ASSERT(currentThread); #ifdef DEBUG if (kDEBUGTransactionThreadPriority != nsISupportsPriority::PRIORITY_NORMAL) { NS_WARNING( "ConnectionPool thread debugging enabled, priority has been " "modified!"); nsCOMPtr thread = do_QueryInterface(currentThread); MOZ_ASSERT(thread); MOZ_ALWAYS_SUCCEEDS(thread->SetPriority(kDEBUGTransactionThreadPriority)); } if (kDEBUGTransactionThreadSleepMS) { NS_WARNING( "TransactionThreadPool thread debugging enabled, sleeping " "after every event!"); } #endif // DEBUG DebugOnly b = SpinEventLoopUntil("ConnectionPool::ThreadRunnable"_ns, [&]() -> bool { if (!mContinueRunning) { return true; } #ifdef DEBUG if (kDEBUGTransactionThreadSleepMS) { MOZ_ALWAYS_TRUE(PR_Sleep(PR_MillisecondsToInterval( kDEBUGTransactionThreadSleepMS)) == PR_SUCCESS); } #endif // DEBUG return false; }); // MSVC can't stringify lambdas, so we have to separate the expression // generating the value from the assert itself. #if DEBUG MOZ_ALWAYS_TRUE(b); #endif } return NS_OK; } ConnectionPool::ThreadInfo::ThreadInfo() { AssertIsOnBackgroundThread(); MOZ_COUNT_CTOR(ConnectionPool::ThreadInfo); } ConnectionPool::ThreadInfo::ThreadInfo(ThreadInfo&& aOther) noexcept : mThread(std::move(aOther.mThread)), mRunnable(std::move(aOther.mRunnable)) { AssertIsOnBackgroundThread(); MOZ_ASSERT(mThread); MOZ_ASSERT(mRunnable); MOZ_COUNT_CTOR(ConnectionPool::ThreadInfo); } ConnectionPool::ThreadInfo::~ThreadInfo() { AssertIsOnBackgroundThread(); MOZ_COUNT_DTOR(ConnectionPool::ThreadInfo); } ConnectionPool::IdleResource::IdleResource(const TimeStamp& aIdleTime) : mIdleTime(aIdleTime) { AssertIsOnBackgroundThread(); MOZ_ASSERT(!aIdleTime.IsNull()); MOZ_COUNT_CTOR(ConnectionPool::IdleResource); } ConnectionPool::IdleResource::~IdleResource() { AssertIsOnBackgroundThread(); MOZ_COUNT_DTOR(ConnectionPool::IdleResource); } ConnectionPool::IdleDatabaseInfo::IdleDatabaseInfo(DatabaseInfo& aDatabaseInfo) : IdleResource( TimeStamp::NowLoRes() + (aDatabaseInfo.mIdle ? TimeDuration::FromMilliseconds(kConnectionIdleMaintenanceMS) : TimeDuration::FromMilliseconds(kConnectionIdleCloseMS))), mDatabaseInfo(WrapNotNullUnchecked(&aDatabaseInfo)) { AssertIsOnBackgroundThread(); MOZ_COUNT_CTOR(ConnectionPool::IdleDatabaseInfo); } ConnectionPool::IdleDatabaseInfo::~IdleDatabaseInfo() { AssertIsOnBackgroundThread(); MOZ_COUNT_DTOR(ConnectionPool::IdleDatabaseInfo); } ConnectionPool::IdleThreadInfo::IdleThreadInfo(ThreadInfo aThreadInfo) : IdleResource(TimeStamp::NowLoRes() + TimeDuration::FromMilliseconds(kConnectionThreadIdleMS)), mThreadInfo(std::move(aThreadInfo)) { AssertIsOnBackgroundThread(); mThreadInfo.AssertValid(); MOZ_COUNT_CTOR(ConnectionPool::IdleThreadInfo); } ConnectionPool::IdleThreadInfo::~IdleThreadInfo() { AssertIsOnBackgroundThread(); MOZ_COUNT_DTOR(ConnectionPool::IdleThreadInfo); } ConnectionPool::TransactionInfo::TransactionInfo( DatabaseInfo& aDatabaseInfo, const nsID& aBackgroundChildLoggingId, const nsACString& aDatabaseId, uint64_t aTransactionId, int64_t aLoggingSerialNumber, const nsTArray& aObjectStoreNames, bool aIsWriteTransaction, TransactionDatabaseOperationBase* aTransactionOp) : mDatabaseInfo(aDatabaseInfo), mBackgroundChildLoggingId(aBackgroundChildLoggingId), mDatabaseId(aDatabaseId), mTransactionId(aTransactionId), mLoggingSerialNumber(aLoggingSerialNumber), mObjectStoreNames(aObjectStoreNames.Clone()), mIsWriteTransaction(aIsWriteTransaction), mRunning(false) { AssertIsOnBackgroundThread(); aDatabaseInfo.mConnectionPool->AssertIsOnOwningThread(); MOZ_COUNT_CTOR(ConnectionPool::TransactionInfo); if (aTransactionOp) { mQueuedRunnables.AppendElement(aTransactionOp); } } ConnectionPool::TransactionInfo::~TransactionInfo() { AssertIsOnBackgroundThread(); MOZ_ASSERT(!mBlockedOn.Count()); MOZ_ASSERT(mQueuedRunnables.IsEmpty()); MOZ_ASSERT(!mRunning); MOZ_ASSERT(mFinished); MOZ_COUNT_DTOR(ConnectionPool::TransactionInfo); } void ConnectionPool::TransactionInfo::AddBlockingTransaction( TransactionInfo& aTransactionInfo) { AssertIsOnBackgroundThread(); // XXX Does it really make sense to have both mBlocking and mBlockingOrdered, // just to reduce the algorithmic complexity of this Contains check? This was // mentioned in the context of Bug 1290853, but no real justification was // given. There was the suggestion of encapsulating this in an // insertion-ordered hashtable implementation, which seems like a good idea. // If we had that, this would be the appropriate data structure to use here. if (mBlocking.EnsureInserted(&aTransactionInfo)) { mBlockingOrdered.AppendElement(WrapNotNullUnchecked(&aTransactionInfo)); } } void ConnectionPool::TransactionInfo::RemoveBlockingTransactions() { AssertIsOnBackgroundThread(); for (const auto blockedInfo : mBlockingOrdered) { blockedInfo->MaybeUnblock(*this); } mBlocking.Clear(); mBlockingOrdered.Clear(); } void ConnectionPool::TransactionInfo::MaybeUnblock( TransactionInfo& aTransactionInfo) { AssertIsOnBackgroundThread(); MOZ_ASSERT(mBlockedOn.Contains(&aTransactionInfo)); mBlockedOn.Remove(&aTransactionInfo); if (mBlockedOn.IsEmpty()) { ConnectionPool* connectionPool = mDatabaseInfo.mConnectionPool; MOZ_ASSERT(connectionPool); connectionPool->AssertIsOnOwningThread(); Unused << connectionPool->ScheduleTransaction( *this, /* aFromQueuedTransactions */ false); } } #if defined(DEBUG) || defined(NS_BUILD_REFCNT_LOGGING) ConnectionPool::TransactionInfoPair::TransactionInfoPair() { AssertIsOnBackgroundThread(); MOZ_COUNT_CTOR(ConnectionPool::TransactionInfoPair); } ConnectionPool::TransactionInfoPair::~TransactionInfoPair() { AssertIsOnBackgroundThread(); MOZ_COUNT_DTOR(ConnectionPool::TransactionInfoPair); } #endif /******************************************************************************* * Metadata classes ******************************************************************************/ bool FullObjectStoreMetadata::HasLiveIndexes() const { AssertIsOnBackgroundThread(); return std::any_of(mIndexes.Values().cbegin(), mIndexes.Values().cend(), [](const auto& entry) { return !entry->mDeleted; }); } SafeRefPtr FullDatabaseMetadata::Duplicate() const { AssertIsOnBackgroundThread(); // FullDatabaseMetadata contains two hash tables of pointers that we need to // duplicate so we can't just use the copy constructor. auto newMetadata = MakeSafeRefPtr(mCommonMetadata); newMetadata->mDatabaseId = mDatabaseId; newMetadata->mFilePath = mFilePath; newMetadata->mNextObjectStoreId = mNextObjectStoreId; newMetadata->mNextIndexId = mNextIndexId; for (const auto& objectStoreEntry : mObjectStores) { const auto& objectStoreValue = objectStoreEntry.GetData(); auto newOSMetadata = MakeSafeRefPtr( objectStoreValue->mCommonMetadata, [&objectStoreValue] { const auto&& srcLocked = objectStoreValue->mAutoIncrementIds.Lock(); return *srcLocked; }()); for (const auto& indexEntry : objectStoreValue->mIndexes) { const auto& value = indexEntry.GetData(); auto newIndexMetadata = MakeSafeRefPtr(); newIndexMetadata->mCommonMetadata = value->mCommonMetadata; if (NS_WARN_IF(!newOSMetadata->mIndexes.InsertOrUpdate( indexEntry.GetKey(), std::move(newIndexMetadata), fallible))) { return nullptr; } } MOZ_ASSERT(objectStoreValue->mIndexes.Count() == newOSMetadata->mIndexes.Count()); if (NS_WARN_IF(!newMetadata->mObjectStores.InsertOrUpdate( objectStoreEntry.GetKey(), std::move(newOSMetadata), fallible))) { return nullptr; } } MOZ_ASSERT(mObjectStores.Count() == newMetadata->mObjectStores.Count()); return newMetadata; } DatabaseLoggingInfo::~DatabaseLoggingInfo() { AssertIsOnBackgroundThread(); if (gLoggingInfoHashtable) { const nsID& backgroundChildLoggingId = mLoggingInfo.backgroundChildLoggingId(); MOZ_ASSERT(gLoggingInfoHashtable->Get(backgroundChildLoggingId) == this); gLoggingInfoHashtable->Remove(backgroundChildLoggingId); } } /******************************************************************************* * Factory ******************************************************************************/ Factory::Factory(RefPtr aLoggingInfo) : mLoggingInfo(std::move(aLoggingInfo)) #ifdef DEBUG , mActorDestroyed(false) #endif { AssertIsOnBackgroundThread(); MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); } Factory::~Factory() { MOZ_ASSERT(mActorDestroyed); } // static SafeRefPtr Factory::Create(const LoggingInfo& aLoggingInfo) { AssertIsOnBackgroundThread(); MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); // Balanced in ActoryDestroy(). IncreaseBusyCount(); MOZ_ASSERT(gLoggingInfoHashtable); RefPtr loggingInfo = gLoggingInfoHashtable->WithEntryHandle( aLoggingInfo.backgroundChildLoggingId(), [&](auto&& entry) { if (entry) { [[maybe_unused]] const auto& loggingInfo = entry.Data(); MOZ_ASSERT(aLoggingInfo.backgroundChildLoggingId() == loggingInfo->Id()); #if !FUZZING NS_WARNING_ASSERTION( aLoggingInfo.nextTransactionSerialNumber() == loggingInfo->mLoggingInfo.nextTransactionSerialNumber(), "NextTransactionSerialNumber doesn't match!"); NS_WARNING_ASSERTION( aLoggingInfo.nextVersionChangeTransactionSerialNumber() == loggingInfo->mLoggingInfo .nextVersionChangeTransactionSerialNumber(), "NextVersionChangeTransactionSerialNumber doesn't match!"); NS_WARNING_ASSERTION( aLoggingInfo.nextRequestSerialNumber() == loggingInfo->mLoggingInfo.nextRequestSerialNumber(), "NextRequestSerialNumber doesn't match!"); #endif // !FUZZING } else { entry.Insert(new DatabaseLoggingInfo(aLoggingInfo)); } return do_AddRef(entry.Data()); }); return MakeSafeRefPtr(std::move(loggingInfo)); } void Factory::ActorDestroy(ActorDestroyReason aWhy) { AssertIsOnBackgroundThread(); MOZ_ASSERT(!mActorDestroyed); #ifdef DEBUG mActorDestroyed = true; #endif // Match the IncreaseBusyCount in Create(). DecreaseBusyCount(); } mozilla::ipc::IPCResult Factory::RecvDeleteMe() { AssertIsOnBackgroundThread(); MOZ_ASSERT(!mActorDestroyed); QM_WARNONLY_TRY(OkIf(PBackgroundIDBFactoryParent::Send__delete__(this))); return IPC_OK(); } PBackgroundIDBFactoryRequestParent* Factory::AllocPBackgroundIDBFactoryRequestParent( const FactoryRequestParams& aParams) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aParams.type() != FactoryRequestParams::T__None); if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread())) { return nullptr; } const CommonFactoryRequestParams* commonParams; switch (aParams.type()) { case FactoryRequestParams::TOpenDatabaseRequestParams: { const OpenDatabaseRequestParams& params = aParams.get_OpenDatabaseRequestParams(); commonParams = ¶ms.commonParams(); break; } case FactoryRequestParams::TDeleteDatabaseRequestParams: { const DeleteDatabaseRequestParams& params = aParams.get_DeleteDatabaseRequestParams(); commonParams = ¶ms.commonParams(); break; } default: MOZ_CRASH("Should never get here!"); } MOZ_ASSERT(commonParams); const DatabaseMetadata& metadata = commonParams->metadata(); if (NS_AUUF_OR_WARN_IF(!IsValidPersistenceType(metadata.persistenceType()))) { return nullptr; } const PrincipalInfo& principalInfo = commonParams->principalInfo(); if (NS_AUUF_OR_WARN_IF(!QuotaManager::IsPrincipalInfoValid(principalInfo))) { IPC_FAIL(this, "Invalid principal!"); return nullptr; } MOZ_ASSERT(principalInfo.type() == PrincipalInfo::TSystemPrincipalInfo || principalInfo.type() == PrincipalInfo::TContentPrincipalInfo); if (NS_AUUF_OR_WARN_IF( principalInfo.type() == PrincipalInfo::TSystemPrincipalInfo && metadata.persistenceType() != PERSISTENCE_TYPE_PERSISTENT)) { return nullptr; } Maybe contentParentId; uint64_t childID = BackgroundParent::GetChildID(Manager()); if (childID) { // If childID is not zero we are dealing with an other-process actor. We // want to initialize OpenDatabaseOp/DeleteDatabaseOp here with the ID // (and later also Database) in that case, so Database::IsOwnedByProcess // can find Databases belonging to a particular content process when // QuotaClient::AbortOperationsForProcess is called which is currently used // to abort operations for content processes only. contentParentId = Some(ContentParentId(childID)); } auto actor = [&]() -> RefPtr { if (aParams.type() == FactoryRequestParams::TOpenDatabaseRequestParams) { return MakeRefPtr(SafeRefPtrFromThis(), contentParentId, *commonParams); } else { return MakeRefPtr(SafeRefPtrFromThis(), contentParentId, *commonParams); } }(); gFactoryOps->AppendElement(actor); // Balanced in CleanupMetadata() which is/must always called by SendResults(). IncreaseBusyCount(); // Transfer ownership to IPDL. return actor.forget().take(); } mozilla::ipc::IPCResult Factory::RecvPBackgroundIDBFactoryRequestConstructor( PBackgroundIDBFactoryRequestParent* aActor, const FactoryRequestParams& aParams) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); MOZ_ASSERT(aParams.type() != FactoryRequestParams::T__None); MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); auto* op = static_cast(aActor); MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(op)); return IPC_OK(); } bool Factory::DeallocPBackgroundIDBFactoryRequestParent( PBackgroundIDBFactoryRequestParent* aActor) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); // Transfer ownership back from IPDL. RefPtr op = dont_AddRef(static_cast(aActor)); return true; } /******************************************************************************* * WaitForTransactionsHelper ******************************************************************************/ void WaitForTransactionsHelper::WaitForTransactions() { MOZ_ASSERT(mState == State::Initial); Unused << this->Run(); } void WaitForTransactionsHelper::MaybeWaitForTransactions() { AssertIsOnBackgroundThread(); MOZ_ASSERT(mState == State::Initial); RefPtr connectionPool = gConnectionPool.get(); if (connectionPool) { mState = State::WaitingForTransactions; connectionPool->WaitForDatabaseToComplete(mDatabaseId, this); return; } CallCallback(); } void WaitForTransactionsHelper::CallCallback() { AssertIsOnBackgroundThread(); MOZ_ASSERT(mState == State::Initial || mState == State::WaitingForTransactions); const nsCOMPtr callback = std::move(mCallback); callback->Run(); mState = State::Complete; } NS_IMETHODIMP WaitForTransactionsHelper::Run() { MOZ_ASSERT(mState != State::Complete); MOZ_ASSERT(mCallback); switch (mState) { case State::Initial: MaybeWaitForTransactions(); break; case State::WaitingForTransactions: CallCallback(); break; default: MOZ_CRASH("Should never get here!"); } return NS_OK; } /******************************************************************************* * Database ******************************************************************************/ Database::Database(SafeRefPtr aFactory, const PrincipalInfo& aPrincipalInfo, const Maybe& aOptionalContentParentId, const quota::OriginMetadata& aOriginMetadata, uint32_t aTelemetryId, SafeRefPtr aMetadata, SafeRefPtr aFileManager, RefPtr aDirectoryLock, bool aInPrivateBrowsing, const Maybe& aMaybeKey) : mFactory(std::move(aFactory)), mMetadata(std::move(aMetadata)), mFileManager(std::move(aFileManager)), mDirectoryLock(std::move(aDirectoryLock)), mPrincipalInfo(aPrincipalInfo), mOptionalContentParentId(aOptionalContentParentId), mOriginMetadata(aOriginMetadata), mId(mMetadata->mDatabaseId), mFilePath(mMetadata->mFilePath), mKey(aMaybeKey), mTelemetryId(aTelemetryId), mPersistenceType(mMetadata->mCommonMetadata.persistenceType()), mInPrivateBrowsing(aInPrivateBrowsing), mBackgroundThread(GetCurrentSerialEventTarget()) #ifdef DEBUG , mAllBlobsUnmapped(false) #endif { AssertIsOnBackgroundThread(); MOZ_ASSERT(mFactory); MOZ_ASSERT(mMetadata); MOZ_ASSERT(mFileManager); MOZ_ASSERT(mDirectoryLock); MOZ_ASSERT(mDirectoryLock->Id() >= 0); mDirectoryLockId = mDirectoryLock->Id(); } template bool Database::InvalidateAll(const nsTBaseHashSet>& aTable) { AssertIsOnBackgroundThread(); const uint32_t count = aTable.Count(); if (!count) { return true; } // XXX Does this really need to be fallible? QM_TRY_INSPECT(const auto& elementsToInvalidate, TransformIntoNewArray( aTable, [](const auto& entry) { return entry; }, fallible), false); IDB_REPORT_INTERNAL_ERR(); for (const auto& elementToInvalidate : elementsToInvalidate) { MOZ_ASSERT(elementToInvalidate); elementToInvalidate->Invalidate(); } return true; } void Database::Invalidate() { AssertIsOnBackgroundThread(); if (mInvalidated) { return; } mInvalidated.Flip(); if (mActorWasAlive && !mActorDestroyed) { Unused << SendInvalidate(); } QM_WARNONLY_TRY(OkIf(InvalidateAll(mTransactions))); MOZ_ALWAYS_TRUE(CloseInternal()); } nsresult Database::EnsureConnection() { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!IsOnBackgroundThread()); AUTO_PROFILER_LABEL("Database::EnsureConnection", DOM); if (!mConnection || !mConnection->HasStorageConnection()) { QM_TRY_UNWRAP(mConnection, gConnectionPool->GetOrCreateConnection(*this)); } AssertIsOnConnectionThread(); return NS_OK; } bool Database::RegisterTransaction(TransactionBase& aTransaction) { AssertIsOnBackgroundThread(); MOZ_ASSERT(!mTransactions.Contains(&aTransaction)); MOZ_ASSERT(mDirectoryLock); MOZ_ASSERT(!mInvalidated); MOZ_ASSERT(!mClosed); if (NS_WARN_IF(!mTransactions.Insert(&aTransaction, fallible))) { return false; } return true; } void Database::UnregisterTransaction(TransactionBase& aTransaction) { AssertIsOnBackgroundThread(); MOZ_ASSERT(mTransactions.Contains(&aTransaction)); mTransactions.Remove(&aTransaction); MaybeCloseConnection(); } void Database::SetActorAlive() { AssertIsOnBackgroundThread(); MOZ_ASSERT(!mActorDestroyed); mActorWasAlive.Flip(); } void Database::MapBlob(const IPCBlob& aIPCBlob, SafeRefPtr aFileInfo) { AssertIsOnBackgroundThread(); const RemoteLazyStream& stream = aIPCBlob.inputStream(); MOZ_ASSERT(stream.type() == RemoteLazyStream::TRemoteLazyInputStream); nsID id{}; MOZ_ALWAYS_SUCCEEDS( stream.get_RemoteLazyInputStream()->GetInternalStreamID(id)); MOZ_ASSERT(!mMappedBlobs.Contains(id)); mMappedBlobs.InsertOrUpdate(id, std::move(aFileInfo)); RefPtr callback = new UnmapBlobCallback(SafeRefPtrFromThis()); auto storage = RemoteLazyInputStreamStorage::Get(); MOZ_ASSERT(storage.isOk()); storage.inspect()->StoreCallback(id, callback); } void Database::Stringify(nsACString& aResult) const { AssertIsOnBackgroundThread(); constexpr auto kQuotaGenericDelimiterString = "|"_ns; aResult.Append( "DirectoryLock:"_ns + IntToCString(!!mDirectoryLock) + kQuotaGenericDelimiterString + // "Transactions:"_ns + IntToCString(mTransactions.Count()) + kQuotaGenericDelimiterString + // "OtherProcessActor:"_ns + IntToCString( BackgroundParent::IsOtherProcessActor(GetBackgroundParent())) + kQuotaGenericDelimiterString + // "Origin:"_ns + AnonymizedOriginString(mOriginMetadata.mOrigin) + kQuotaGenericDelimiterString + // "PersistenceType:"_ns + PersistenceTypeToString(mPersistenceType) + kQuotaGenericDelimiterString + // "Closed:"_ns + IntToCString(static_cast(mClosed)) + kQuotaGenericDelimiterString + // "Invalidated:"_ns + IntToCString(static_cast(mInvalidated)) + kQuotaGenericDelimiterString + // "ActorWasAlive:"_ns + IntToCString(static_cast(mActorWasAlive)) + kQuotaGenericDelimiterString + // "ActorDestroyed:"_ns + IntToCString(static_cast(mActorDestroyed))); } SafeRefPtr Database::GetBlob(const IPCBlob& aIPCBlob) { AssertIsOnBackgroundThread(); RefPtr lazyStream; switch (aIPCBlob.inputStream().type()) { case RemoteLazyStream::TIPCStream: { const InputStreamParams& inputStreamParams = aIPCBlob.inputStream().get_IPCStream().stream(); if (inputStreamParams.type() != InputStreamParams::TRemoteLazyInputStreamParams) { return nullptr; } lazyStream = inputStreamParams.get_RemoteLazyInputStreamParams().stream(); break; } case RemoteLazyStream::TRemoteLazyInputStream: lazyStream = aIPCBlob.inputStream().get_RemoteLazyInputStream(); break; default: MOZ_ASSERT_UNREACHABLE("Unknown RemoteLazyStream type"); return nullptr; } if (!lazyStream) { MOZ_ASSERT_UNREACHABLE("Unexpected null stream"); return nullptr; } nsID id{}; nsresult rv = lazyStream->GetInternalStreamID(id); if (NS_FAILED(rv)) { MOZ_ASSERT_UNREACHABLE( "Received RemoteLazyInputStream doesn't have an actor connection"); return nullptr; } const auto fileInfo = mMappedBlobs.Lookup(id); return fileInfo ? fileInfo->clonePtr() : nullptr; } void Database::UnmapBlob(const nsID& aID) { AssertIsOnBackgroundThread(); MOZ_ASSERT_IF(!mAllBlobsUnmapped, mMappedBlobs.Contains(aID)); mMappedBlobs.Remove(aID); } void Database::UnmapAllBlobs() { AssertIsOnBackgroundThread(); #ifdef DEBUG mAllBlobsUnmapped = true; #endif mMappedBlobs.Clear(); } bool Database::CloseInternal() { AssertIsOnBackgroundThread(); if (mClosed) { if (NS_WARN_IF(!IsInvalidated())) { // Signal misbehaving child for sending the close message twice. return false; } // Ignore harmless race when we just invalidated the database. return true; } mClosed.Flip(); if (gConnectionPool) { gConnectionPool->CloseDatabaseWhenIdle(Id()); } DatabaseActorInfo* info; MOZ_ALWAYS_TRUE(gLiveDatabaseHashtable->Get(Id(), &info)); MOZ_ASSERT(info->mLiveDatabases.Contains(this)); if (info->mWaitingFactoryOp) { info->mWaitingFactoryOp->NoteDatabaseClosed(this); } MaybeCloseConnection(); return true; } void Database::MaybeCloseConnection() { AssertIsOnBackgroundThread(); if (!mTransactions.Count() && IsClosed() && mDirectoryLock) { nsCOMPtr callback = NewRunnableMethod("dom::indexedDB::Database::ConnectionClosedCallback", this, &Database::ConnectionClosedCallback); RefPtr helper = new WaitForTransactionsHelper(Id(), callback); helper->WaitForTransactions(); } } void Database::ConnectionClosedCallback() { AssertIsOnBackgroundThread(); MOZ_ASSERT(mClosed); MOZ_ASSERT(!mTransactions.Count()); mDirectoryLock = nullptr; CleanupMetadata(); UnmapAllBlobs(); if (IsInvalidated() && IsActorAlive()) { // Step 3 and 4 of "5.2 Closing a Database": // 1. Wait for all transactions to complete. // 2. Fire a close event if forced flag is set, i.e., IsInvalidated() in our // implementation. Unused << SendCloseAfterInvalidationComplete(); } } void Database::CleanupMetadata() { AssertIsOnBackgroundThread(); DatabaseActorInfo* info; MOZ_ALWAYS_TRUE(gLiveDatabaseHashtable->Get(Id(), &info)); MOZ_ALWAYS_TRUE(info->mLiveDatabases.RemoveElement(this)); QuotaManager::MaybeRecordQuotaClientShutdownStep( quota::Client::IDB, "Live database entry removed"_ns); if (info->mLiveDatabases.IsEmpty()) { MOZ_ASSERT(!info->mWaitingFactoryOp || !info->mWaitingFactoryOp->HasBlockedDatabases()); gLiveDatabaseHashtable->Remove(Id()); QuotaManager::MaybeRecordQuotaClientShutdownStep( quota::Client::IDB, "gLiveDatabaseHashtable entry removed"_ns); } // Match the IncreaseBusyCount in OpenDatabaseOp::EnsureDatabaseActor(). DecreaseBusyCount(); } void Database::ActorDestroy(ActorDestroyReason aWhy) { AssertIsOnBackgroundThread(); mActorDestroyed.Flip(); if (!IsInvalidated()) { Invalidate(); } } PBackgroundIDBDatabaseFileParent* Database::AllocPBackgroundIDBDatabaseFileParent(const IPCBlob& aIPCBlob) { AssertIsOnBackgroundThread(); SafeRefPtr fileInfo = GetBlob(aIPCBlob); RefPtr actor; if (fileInfo) { actor = new DatabaseFile(std::move(fileInfo)); } else { // This is a blob we haven't seen before. fileInfo = mFileManager->CreateFileInfo(); if (NS_WARN_IF(!fileInfo)) { return nullptr; } actor = new DatabaseFile(IPCBlobUtils::Deserialize(aIPCBlob), std::move(fileInfo)); } MOZ_ASSERT(actor); return actor.forget().take(); } bool Database::DeallocPBackgroundIDBDatabaseFileParent( PBackgroundIDBDatabaseFileParent* aActor) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); RefPtr actor = dont_AddRef(static_cast(aActor)); return true; } already_AddRefed Database::AllocPBackgroundIDBTransactionParent( const nsTArray& aObjectStoreNames, const Mode& aMode) { AssertIsOnBackgroundThread(); // Once a database is closed it must not try to open new transactions. if (NS_WARN_IF(mClosed)) { MOZ_ASSERT_UNLESS_FUZZING(mInvalidated); return nullptr; } if (NS_AUUF_OR_WARN_IF(aObjectStoreNames.IsEmpty())) { return nullptr; } if (NS_AUUF_OR_WARN_IF(aMode != IDBTransaction::Mode::ReadOnly && aMode != IDBTransaction::Mode::ReadWrite && aMode != IDBTransaction::Mode::ReadWriteFlush && aMode != IDBTransaction::Mode::Cleanup)) { return nullptr; } const ObjectStoreTable& objectStores = mMetadata->mObjectStores; const uint32_t nameCount = aObjectStoreNames.Length(); if (NS_AUUF_OR_WARN_IF(nameCount > objectStores.Count())) { return nullptr; } QM_TRY_UNWRAP( auto objectStoreMetadatas, TransformIntoNewArrayAbortOnErr( aObjectStoreNames, [lastName = Maybe{}, &objectStores](const nsString& name) mutable -> mozilla::Result, nsresult> { if (lastName) { // Make sure that this name is sorted properly and not a // duplicate. if (NS_AUUF_OR_WARN_IF(name <= lastName.ref())) { return Err(NS_ERROR_FAILURE); } } lastName = SomeRef(name); const auto foundIt = std::find_if(objectStores.cbegin(), objectStores.cend(), [&name](const auto& entry) { const auto& value = entry.GetData(); MOZ_ASSERT(entry.GetKey()); return name == value->mCommonMetadata.name() && !value->mDeleted; }); if (foundIt == objectStores.cend()) { MOZ_ASSERT_UNLESS_FUZZING(false, "ObjectStore not found."); return Err(NS_ERROR_FAILURE); } return foundIt->GetData().clonePtr(); }, fallible), nullptr); return MakeSafeRefPtr(SafeRefPtrFromThis(), aMode, std::move(objectStoreMetadatas)) .forget(); } mozilla::ipc::IPCResult Database::RecvPBackgroundIDBTransactionConstructor( PBackgroundIDBTransactionParent* aActor, nsTArray&& aObjectStoreNames, const Mode& aMode) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); MOZ_ASSERT(!aObjectStoreNames.IsEmpty()); MOZ_ASSERT(aMode == IDBTransaction::Mode::ReadOnly || aMode == IDBTransaction::Mode::ReadWrite || aMode == IDBTransaction::Mode::ReadWriteFlush || aMode == IDBTransaction::Mode::Cleanup); MOZ_ASSERT(!mClosed); if (IsInvalidated()) { // This is an expected race. We don't want the child to die here, just don't // actually do any work. return IPC_OK(); } if (!gConnectionPool) { gConnectionPool = new ConnectionPool(); } auto* transaction = static_cast(aActor); RefPtr startOp = new StartTransactionOp( SafeRefPtr{transaction, AcquireStrongRefFromRawPtr{}}); uint64_t transactionId = startOp->StartOnConnectionPool( GetLoggingInfo()->Id(), mMetadata->mDatabaseId, transaction->LoggingSerialNumber(), aObjectStoreNames, aMode != IDBTransaction::Mode::ReadOnly); transaction->Init(transactionId); if (NS_WARN_IF(!RegisterTransaction(*transaction))) { IDB_REPORT_INTERNAL_ERR(); transaction->Abort(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR, /* aForce */ false); return IPC_OK(); } return IPC_OK(); } mozilla::ipc::IPCResult Database::RecvDeleteMe() { AssertIsOnBackgroundThread(); MOZ_ASSERT(!mActorDestroyed); QM_WARNONLY_TRY(OkIf(PBackgroundIDBDatabaseParent::Send__delete__(this))); return IPC_OK(); } mozilla::ipc::IPCResult Database::RecvBlocked() { AssertIsOnBackgroundThread(); if (NS_WARN_IF(mClosed)) { return IPC_FAIL(this, "Database already closed!"); } DatabaseActorInfo* info; MOZ_ALWAYS_TRUE(gLiveDatabaseHashtable->Get(Id(), &info)); MOZ_ASSERT(info->mLiveDatabases.Contains(this)); if (NS_WARN_IF(!info->mWaitingFactoryOp)) { return IPC_FAIL(this, "Database info has no mWaitingFactoryOp!"); } info->mWaitingFactoryOp->NoteDatabaseBlocked(this); return IPC_OK(); } mozilla::ipc::IPCResult Database::RecvClose() { AssertIsOnBackgroundThread(); if (NS_WARN_IF(!CloseInternal())) { return IPC_FAIL(this, "CloseInternal failed!"); } return IPC_OK(); } void Database::StartTransactionOp::RunOnConnectionThread() { MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(!HasFailed()); IDB_LOG_MARK_PARENT_TRANSACTION("Beginning database work", "DB Start", IDB_LOG_ID_STRING(mBackgroundChildLoggingId), mTransactionLoggingSerialNumber); TransactionDatabaseOperationBase::RunOnConnectionThread(); } nsresult Database::StartTransactionOp::DoDatabaseWork( DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); Transaction().SetActiveOnConnectionThread(); if (Transaction().GetMode() == IDBTransaction::Mode::Cleanup) { DebugOnly rv = aConnection->DisableQuotaChecks(); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "DisableQuotaChecks failed, trying to continue " "cleanup transaction with quota checks enabled"); } if (Transaction().GetMode() != IDBTransaction::Mode::ReadOnly) { QM_TRY(MOZ_TO_RESULT(aConnection->BeginWriteTransaction())); } return NS_OK; } nsresult Database::StartTransactionOp::SendSuccessResult() { // We don't need to do anything here. return NS_OK; } bool Database::StartTransactionOp::SendFailureResult( nsresult /* aResultCode */) { IDB_REPORT_INTERNAL_ERR(); // Abort the transaction. return false; } void Database::StartTransactionOp::Cleanup() { #ifdef DEBUG // StartTransactionOp is not a normal database operation that is tied to an // actor. Do this to make our assertions happy. NoteActorDestroyed(); #endif TransactionDatabaseOperationBase::Cleanup(); } /******************************************************************************* * TransactionBase ******************************************************************************/ TransactionBase::TransactionBase(SafeRefPtr aDatabase, Mode aMode) : mDatabase(std::move(aDatabase)), mDatabaseId(mDatabase->Id()), mLoggingSerialNumber( mDatabase->GetLoggingInfo()->NextTransactionSN(aMode)), mActiveRequestCount(0), mInvalidatedOnAnyThread(false), mMode(aMode), mResultCode(NS_OK) { AssertIsOnBackgroundThread(); MOZ_ASSERT(mDatabase); MOZ_ASSERT(mLoggingSerialNumber); } TransactionBase::~TransactionBase() { MOZ_ASSERT(!mActiveRequestCount); MOZ_ASSERT(mActorDestroyed); MOZ_ASSERT_IF(mInitialized, mCommittedOrAborted); } void TransactionBase::Abort(nsresult aResultCode, bool aForce) { AssertIsOnBackgroundThread(); MOZ_ASSERT(NS_FAILED(aResultCode)); if (NS_SUCCEEDED(mResultCode)) { mResultCode = aResultCode; } if (aForce) { mForceAborted.EnsureFlipped(); } MaybeCommitOrAbort(); } mozilla::ipc::IPCResult TransactionBase::RecvCommit( IProtocol* aActor, const Maybe aLastRequest) { AssertIsOnBackgroundThread(); if (NS_WARN_IF(mCommitOrAbortReceived)) { return IPC_FAIL( aActor, "Attempt to commit an already comitted/aborted transaction!"); } mCommitOrAbortReceived.Flip(); mLastRequestBeforeCommit.init(aLastRequest); MaybeCommitOrAbort(); return IPC_OK(); } mozilla::ipc::IPCResult TransactionBase::RecvAbort(IProtocol* aActor, nsresult aResultCode) { AssertIsOnBackgroundThread(); if (NS_WARN_IF(NS_SUCCEEDED(aResultCode))) { return IPC_FAIL(aActor, "aResultCode must not be a success code!"); } if (NS_WARN_IF(NS_ERROR_GET_MODULE(aResultCode) != NS_ERROR_MODULE_DOM_INDEXEDDB)) { return IPC_FAIL(aActor, "aResultCode does not refer to IndexedDB!"); } if (NS_WARN_IF(mCommitOrAbortReceived)) { return IPC_FAIL( aActor, "Attempt to abort an already comitted/aborted transaction!"); } mCommitOrAbortReceived.Flip(); Abort(aResultCode, /* aForce */ false); return IPC_OK(); } void TransactionBase::CommitOrAbort() { AssertIsOnBackgroundThread(); mCommittedOrAborted.Flip(); if (!mInitialized) { return; } // In case of a failed request that was started after committing was // initiated, abort (cf. // https://w3c.github.io/IndexedDB/#async-execute-request step 5.3 vs. 5.4). // Note this can only happen here when we are committing explicitly, otherwise // the decision is made by the child. if (NS_SUCCEEDED(mResultCode) && mLastFailedRequest && *mLastRequestBeforeCommit && *mLastFailedRequest >= **mLastRequestBeforeCommit) { mResultCode = NS_ERROR_DOM_INDEXEDDB_ABORT_ERR; } RefPtr commitOp = new CommitOp(SafeRefPtrFromThis(), ClampResultCode(mResultCode)); gConnectionPool->Finish(TransactionId(), commitOp); } SafeRefPtr TransactionBase::GetMetadataForObjectStoreId( IndexOrObjectStoreId aObjectStoreId) const { AssertIsOnBackgroundThread(); MOZ_ASSERT(aObjectStoreId); if (!aObjectStoreId) { return nullptr; } auto metadata = mDatabase->Metadata().mObjectStores.Lookup(aObjectStoreId); if (!metadata || (*metadata)->mDeleted) { return nullptr; } MOZ_ASSERT((*metadata)->mCommonMetadata.id() == aObjectStoreId); return metadata->clonePtr(); } SafeRefPtr TransactionBase::GetMetadataForIndexId( FullObjectStoreMetadata& aObjectStoreMetadata, IndexOrObjectStoreId aIndexId) const { AssertIsOnBackgroundThread(); MOZ_ASSERT(aIndexId); if (!aIndexId) { return nullptr; } auto metadata = aObjectStoreMetadata.mIndexes.Lookup(aIndexId); if (!metadata || (*metadata)->mDeleted) { return nullptr; } MOZ_ASSERT((*metadata)->mCommonMetadata.id() == aIndexId); return metadata->clonePtr(); } void TransactionBase::NoteModifiedAutoIncrementObjectStore( const SafeRefPtr& aMetadata) { AssertIsOnConnectionThread(); if (!mModifiedAutoIncrementObjectStoreMetadataArray.Contains(aMetadata)) { mModifiedAutoIncrementObjectStoreMetadataArray.AppendElement( aMetadata.clonePtr()); } } void TransactionBase::ForgetModifiedAutoIncrementObjectStore( FullObjectStoreMetadata& aMetadata) { AssertIsOnConnectionThread(); mModifiedAutoIncrementObjectStoreMetadataArray.RemoveElement(&aMetadata); } bool TransactionBase::VerifyRequestParams(const RequestParams& aParams) const { AssertIsOnBackgroundThread(); MOZ_ASSERT(aParams.type() != RequestParams::T__None); switch (aParams.type()) { case RequestParams::TObjectStoreAddParams: { const ObjectStoreAddPutParams& params = aParams.get_ObjectStoreAddParams().commonParams(); if (NS_AUUF_OR_WARN_IF(!VerifyRequestParams(params))) { return false; } break; } case RequestParams::TObjectStorePutParams: { const ObjectStoreAddPutParams& params = aParams.get_ObjectStorePutParams().commonParams(); if (NS_AUUF_OR_WARN_IF(!VerifyRequestParams(params))) { return false; } break; } case RequestParams::TObjectStoreGetParams: { const ObjectStoreGetParams& params = aParams.get_ObjectStoreGetParams(); const SafeRefPtr objectStoreMetadata = GetMetadataForObjectStoreId(params.objectStoreId()); if (NS_AUUF_OR_WARN_IF(!objectStoreMetadata)) { return false; } if (NS_AUUF_OR_WARN_IF(!VerifyRequestParams(params.keyRange()))) { return false; } break; } case RequestParams::TObjectStoreGetKeyParams: { const ObjectStoreGetKeyParams& params = aParams.get_ObjectStoreGetKeyParams(); const SafeRefPtr objectStoreMetadata = GetMetadataForObjectStoreId(params.objectStoreId()); if (NS_AUUF_OR_WARN_IF(!objectStoreMetadata)) { return false; } if (NS_AUUF_OR_WARN_IF(!VerifyRequestParams(params.keyRange()))) { return false; } break; } case RequestParams::TObjectStoreGetAllParams: { const ObjectStoreGetAllParams& params = aParams.get_ObjectStoreGetAllParams(); const SafeRefPtr objectStoreMetadata = GetMetadataForObjectStoreId(params.objectStoreId()); if (NS_AUUF_OR_WARN_IF(!objectStoreMetadata)) { return false; } if (NS_AUUF_OR_WARN_IF(!VerifyRequestParams(params.optionalKeyRange()))) { return false; } break; } case RequestParams::TObjectStoreGetAllKeysParams: { const ObjectStoreGetAllKeysParams& params = aParams.get_ObjectStoreGetAllKeysParams(); const SafeRefPtr objectStoreMetadata = GetMetadataForObjectStoreId(params.objectStoreId()); if (NS_AUUF_OR_WARN_IF(!objectStoreMetadata)) { return false; } if (NS_AUUF_OR_WARN_IF(!VerifyRequestParams(params.optionalKeyRange()))) { return false; } break; } case RequestParams::TObjectStoreDeleteParams: { if (NS_AUUF_OR_WARN_IF(mMode != IDBTransaction::Mode::ReadWrite && mMode != IDBTransaction::Mode::ReadWriteFlush && mMode != IDBTransaction::Mode::Cleanup && mMode != IDBTransaction::Mode::VersionChange)) { return false; } const ObjectStoreDeleteParams& params = aParams.get_ObjectStoreDeleteParams(); const SafeRefPtr objectStoreMetadata = GetMetadataForObjectStoreId(params.objectStoreId()); if (NS_AUUF_OR_WARN_IF(!objectStoreMetadata)) { return false; } if (NS_AUUF_OR_WARN_IF(!VerifyRequestParams(params.keyRange()))) { return false; } break; } case RequestParams::TObjectStoreClearParams: { if (NS_AUUF_OR_WARN_IF(mMode != IDBTransaction::Mode::ReadWrite && mMode != IDBTransaction::Mode::ReadWriteFlush && mMode != IDBTransaction::Mode::Cleanup && mMode != IDBTransaction::Mode::VersionChange)) { return false; } const ObjectStoreClearParams& params = aParams.get_ObjectStoreClearParams(); const SafeRefPtr objectStoreMetadata = GetMetadataForObjectStoreId(params.objectStoreId()); if (NS_AUUF_OR_WARN_IF(!objectStoreMetadata)) { return false; } break; } case RequestParams::TObjectStoreCountParams: { const ObjectStoreCountParams& params = aParams.get_ObjectStoreCountParams(); const SafeRefPtr objectStoreMetadata = GetMetadataForObjectStoreId(params.objectStoreId()); if (NS_AUUF_OR_WARN_IF(!objectStoreMetadata)) { return false; } if (NS_AUUF_OR_WARN_IF(!VerifyRequestParams(params.optionalKeyRange()))) { return false; } break; } case RequestParams::TIndexGetParams: { const IndexGetParams& params = aParams.get_IndexGetParams(); const SafeRefPtr objectStoreMetadata = GetMetadataForObjectStoreId(params.objectStoreId()); if (NS_AUUF_OR_WARN_IF(!objectStoreMetadata)) { return false; } const SafeRefPtr indexMetadata = GetMetadataForIndexId(*objectStoreMetadata, params.indexId()); if (NS_AUUF_OR_WARN_IF(!indexMetadata)) { return false; } if (NS_AUUF_OR_WARN_IF(!VerifyRequestParams(params.keyRange()))) { return false; } break; } case RequestParams::TIndexGetKeyParams: { const IndexGetKeyParams& params = aParams.get_IndexGetKeyParams(); const SafeRefPtr objectStoreMetadata = GetMetadataForObjectStoreId(params.objectStoreId()); if (NS_AUUF_OR_WARN_IF(!objectStoreMetadata)) { return false; } const SafeRefPtr indexMetadata = GetMetadataForIndexId(*objectStoreMetadata, params.indexId()); if (NS_AUUF_OR_WARN_IF(!indexMetadata)) { return false; } if (NS_AUUF_OR_WARN_IF(!VerifyRequestParams(params.keyRange()))) { return false; } break; } case RequestParams::TIndexGetAllParams: { const IndexGetAllParams& params = aParams.get_IndexGetAllParams(); const SafeRefPtr objectStoreMetadata = GetMetadataForObjectStoreId(params.objectStoreId()); if (NS_AUUF_OR_WARN_IF(!objectStoreMetadata)) { return false; } const SafeRefPtr indexMetadata = GetMetadataForIndexId(*objectStoreMetadata, params.indexId()); if (NS_AUUF_OR_WARN_IF(!indexMetadata)) { return false; } if (NS_AUUF_OR_WARN_IF(!VerifyRequestParams(params.optionalKeyRange()))) { return false; } break; } case RequestParams::TIndexGetAllKeysParams: { const IndexGetAllKeysParams& params = aParams.get_IndexGetAllKeysParams(); const SafeRefPtr objectStoreMetadata = GetMetadataForObjectStoreId(params.objectStoreId()); if (NS_AUUF_OR_WARN_IF(!objectStoreMetadata)) { return false; } const SafeRefPtr indexMetadata = GetMetadataForIndexId(*objectStoreMetadata, params.indexId()); if (NS_AUUF_OR_WARN_IF(!indexMetadata)) { return false; } if (NS_AUUF_OR_WARN_IF(!VerifyRequestParams(params.optionalKeyRange()))) { return false; } break; } case RequestParams::TIndexCountParams: { const IndexCountParams& params = aParams.get_IndexCountParams(); const SafeRefPtr objectStoreMetadata = GetMetadataForObjectStoreId(params.objectStoreId()); if (NS_AUUF_OR_WARN_IF(!objectStoreMetadata)) { return false; } const SafeRefPtr indexMetadata = GetMetadataForIndexId(*objectStoreMetadata, params.indexId()); if (NS_AUUF_OR_WARN_IF(!indexMetadata)) { return false; } if (NS_AUUF_OR_WARN_IF(!VerifyRequestParams(params.optionalKeyRange()))) { return false; } break; } default: MOZ_CRASH("Should never get here!"); } return true; } bool TransactionBase::VerifyRequestParams( const SerializedKeyRange& aParams) const { AssertIsOnBackgroundThread(); // XXX Check more here? if (aParams.isOnly()) { if (NS_AUUF_OR_WARN_IF(aParams.lower().IsUnset())) { return false; } if (NS_AUUF_OR_WARN_IF(!aParams.upper().IsUnset())) { return false; } if (NS_AUUF_OR_WARN_IF(aParams.lowerOpen())) { return false; } if (NS_AUUF_OR_WARN_IF(aParams.upperOpen())) { return false; } } else if (NS_AUUF_OR_WARN_IF(aParams.lower().IsUnset() && aParams.upper().IsUnset())) { return false; } return true; } bool TransactionBase::VerifyRequestParams( const ObjectStoreAddPutParams& aParams) const { AssertIsOnBackgroundThread(); if (NS_AUUF_OR_WARN_IF(mMode != IDBTransaction::Mode::ReadWrite && mMode != IDBTransaction::Mode::ReadWriteFlush && mMode != IDBTransaction::Mode::VersionChange)) { return false; } SafeRefPtr objMetadata = GetMetadataForObjectStoreId(aParams.objectStoreId()); if (NS_AUUF_OR_WARN_IF(!objMetadata)) { return false; } if (NS_AUUF_OR_WARN_IF(!aParams.cloneInfo().data().data.Size())) { return false; } if (objMetadata->mCommonMetadata.autoIncrement() && objMetadata->mCommonMetadata.keyPath().IsValid() && aParams.key().IsUnset()) { const SerializedStructuredCloneWriteInfo& cloneInfo = aParams.cloneInfo(); if (NS_AUUF_OR_WARN_IF(!cloneInfo.offsetToKeyProp())) { return false; } if (NS_AUUF_OR_WARN_IF(cloneInfo.data().data.Size() < sizeof(uint64_t))) { return false; } if (NS_AUUF_OR_WARN_IF(cloneInfo.offsetToKeyProp() > (cloneInfo.data().data.Size() - sizeof(uint64_t)))) { return false; } } else if (NS_AUUF_OR_WARN_IF(aParams.cloneInfo().offsetToKeyProp())) { return false; } for (const auto& updateInfo : aParams.indexUpdateInfos()) { SafeRefPtr indexMetadata = GetMetadataForIndexId(*objMetadata, updateInfo.indexId()); if (NS_AUUF_OR_WARN_IF(!indexMetadata)) { return false; } if (NS_AUUF_OR_WARN_IF(updateInfo.value().IsUnset())) { return false; } MOZ_ASSERT(!updateInfo.value().GetBuffer().IsEmpty()); } for (const FileAddInfo& fileAddInfo : aParams.fileAddInfos()) { const PBackgroundIDBDatabaseFileParent* file = fileAddInfo.file().AsParent(); switch (fileAddInfo.type()) { case StructuredCloneFileBase::eBlob: if (NS_AUUF_OR_WARN_IF(!file)) { return false; } break; case StructuredCloneFileBase::eMutableFile: { return false; } case StructuredCloneFileBase::eStructuredClone: case StructuredCloneFileBase::eWasmBytecode: case StructuredCloneFileBase::eWasmCompiled: case StructuredCloneFileBase::eEndGuard: MOZ_ASSERT_UNLESS_FUZZING(false, "Unsupported."); return false; default: MOZ_CRASH("Should never get here!"); } } return true; } bool TransactionBase::VerifyRequestParams( const Maybe& aParams) const { AssertIsOnBackgroundThread(); if (aParams.isSome()) { if (NS_AUUF_OR_WARN_IF(!VerifyRequestParams(aParams.ref()))) { return false; } } return true; } void TransactionBase::NoteActiveRequest() { AssertIsOnBackgroundThread(); MOZ_ASSERT(mActiveRequestCount < UINT64_MAX); mActiveRequestCount++; } void TransactionBase::NoteFinishedRequest(const int64_t aRequestId, const nsresult aResultCode) { AssertIsOnBackgroundThread(); MOZ_ASSERT(mActiveRequestCount); mActiveRequestCount--; if (NS_FAILED(aResultCode)) { mLastFailedRequest = Some(aRequestId); } MaybeCommitOrAbort(); } void TransactionBase::Invalidate() { AssertIsOnBackgroundThread(); MOZ_ASSERT(mInvalidated == mInvalidatedOnAnyThread); if (!mInvalidated) { mInvalidated.Flip(); mInvalidatedOnAnyThread = true; Abort(NS_ERROR_DOM_INDEXEDDB_ABORT_ERR, /* aForce */ false); } } PBackgroundIDBRequestParent* TransactionBase::AllocRequest( RequestParams&& aParams, bool aTrustParams) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aParams.type() != RequestParams::T__None); #ifdef DEBUG // Always verify parameters in DEBUG builds! aTrustParams = false; #endif if (NS_AUUF_OR_WARN_IF(!aTrustParams && !VerifyRequestParams(aParams))) { return nullptr; } if (NS_AUUF_OR_WARN_IF(mCommitOrAbortReceived)) { return nullptr; } RefPtr actor; switch (aParams.type()) { case RequestParams::TObjectStoreAddParams: case RequestParams::TObjectStorePutParams: actor = new ObjectStoreAddOrPutRequestOp(SafeRefPtrFromThis(), std::move(aParams)); break; case RequestParams::TObjectStoreGetParams: actor = new ObjectStoreGetRequestOp(SafeRefPtrFromThis(), aParams, /* aGetAll */ false); break; case RequestParams::TObjectStoreGetAllParams: actor = new ObjectStoreGetRequestOp(SafeRefPtrFromThis(), aParams, /* aGetAll */ true); break; case RequestParams::TObjectStoreGetKeyParams: actor = new ObjectStoreGetKeyRequestOp(SafeRefPtrFromThis(), aParams, /* aGetAll */ false); break; case RequestParams::TObjectStoreGetAllKeysParams: actor = new ObjectStoreGetKeyRequestOp(SafeRefPtrFromThis(), aParams, /* aGetAll */ true); break; case RequestParams::TObjectStoreDeleteParams: actor = new ObjectStoreDeleteRequestOp( SafeRefPtrFromThis(), aParams.get_ObjectStoreDeleteParams()); break; case RequestParams::TObjectStoreClearParams: actor = new ObjectStoreClearRequestOp( SafeRefPtrFromThis(), aParams.get_ObjectStoreClearParams()); break; case RequestParams::TObjectStoreCountParams: actor = new ObjectStoreCountRequestOp( SafeRefPtrFromThis(), aParams.get_ObjectStoreCountParams()); break; case RequestParams::TIndexGetParams: actor = new IndexGetRequestOp(SafeRefPtrFromThis(), aParams, /* aGetAll */ false); break; case RequestParams::TIndexGetKeyParams: actor = new IndexGetKeyRequestOp(SafeRefPtrFromThis(), aParams, /* aGetAll */ false); break; case RequestParams::TIndexGetAllParams: actor = new IndexGetRequestOp(SafeRefPtrFromThis(), aParams, /* aGetAll */ true); break; case RequestParams::TIndexGetAllKeysParams: actor = new IndexGetKeyRequestOp(SafeRefPtrFromThis(), aParams, /* aGetAll */ true); break; case RequestParams::TIndexCountParams: actor = new IndexCountRequestOp(SafeRefPtrFromThis(), aParams); break; default: MOZ_CRASH("Should never get here!"); } MOZ_ASSERT(actor); // Transfer ownership to IPDL. return actor.forget().take(); } bool TransactionBase::StartRequest(PBackgroundIDBRequestParent* aActor) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); auto* op = static_cast(aActor); if (NS_WARN_IF(!op->Init(*this))) { op->Cleanup(); return false; } op->DispatchToConnectionPool(); return true; } bool TransactionBase::DeallocRequest( PBackgroundIDBRequestParent* const aActor) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); // Transfer ownership back from IPDL. const RefPtr actor = dont_AddRef(static_cast(aActor)); return true; } already_AddRefed TransactionBase::AllocCursor( const OpenCursorParams& aParams, bool aTrustParams) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aParams.type() != OpenCursorParams::T__None); #ifdef DEBUG // Always verify parameters in DEBUG builds! aTrustParams = false; #endif const OpenCursorParams::Type type = aParams.type(); SafeRefPtr objectStoreMetadata; SafeRefPtr indexMetadata; CursorBase::Direction direction; // First extract the parameters common to all open cursor variants. const auto& commonParams = GetCommonOpenCursorParams(aParams); objectStoreMetadata = GetMetadataForObjectStoreId(commonParams.objectStoreId()); if (NS_AUUF_OR_WARN_IF(!objectStoreMetadata)) { return nullptr; } if (aTrustParams && NS_AUUF_OR_WARN_IF(!VerifyRequestParams( commonParams.optionalKeyRange()))) { return nullptr; } direction = commonParams.direction(); // Now, for the index open cursor variants, extract the additional parameter. if (type == OpenCursorParams::TIndexOpenCursorParams || type == OpenCursorParams::TIndexOpenKeyCursorParams) { const auto& commonIndexParams = GetCommonIndexOpenCursorParams(aParams); indexMetadata = GetMetadataForIndexId(*objectStoreMetadata, commonIndexParams.indexId()); if (NS_AUUF_OR_WARN_IF(!indexMetadata)) { return nullptr; } } if (NS_AUUF_OR_WARN_IF(mCommitOrAbortReceived)) { return nullptr; } // Create Cursor and transfer ownership to IPDL. switch (type) { case OpenCursorParams::TObjectStoreOpenCursorParams: MOZ_ASSERT(!indexMetadata); return MakeAndAddRef>( SafeRefPtrFromThis(), std::move(objectStoreMetadata), direction, CursorBase::ConstructFromTransactionBase{}); case OpenCursorParams::TObjectStoreOpenKeyCursorParams: MOZ_ASSERT(!indexMetadata); return MakeAndAddRef>( SafeRefPtrFromThis(), std::move(objectStoreMetadata), direction, CursorBase::ConstructFromTransactionBase{}); case OpenCursorParams::TIndexOpenCursorParams: return MakeAndAddRef>( SafeRefPtrFromThis(), std::move(objectStoreMetadata), std::move(indexMetadata), direction, CursorBase::ConstructFromTransactionBase{}); case OpenCursorParams::TIndexOpenKeyCursorParams: return MakeAndAddRef>( SafeRefPtrFromThis(), std::move(objectStoreMetadata), std::move(indexMetadata), direction, CursorBase::ConstructFromTransactionBase{}); default: MOZ_CRASH("Cannot get here."); } } bool TransactionBase::StartCursor(PBackgroundIDBCursorParent* const aActor, const OpenCursorParams& aParams) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); MOZ_ASSERT(aParams.type() != OpenCursorParams::T__None); auto* const op = static_cast(aActor); if (NS_WARN_IF(!op->Start(aParams))) { return false; } return true; } /******************************************************************************* * NormalTransaction ******************************************************************************/ NormalTransaction::NormalTransaction( SafeRefPtr aDatabase, TransactionBase::Mode aMode, nsTArray>&& aObjectStores) : TransactionBase(std::move(aDatabase), aMode), mObjectStores{std::move(aObjectStores)} { AssertIsOnBackgroundThread(); MOZ_ASSERT(!mObjectStores.IsEmpty()); } bool NormalTransaction::IsSameProcessActor() { AssertIsOnBackgroundThread(); PBackgroundParent* const actor = Manager()->Manager()->Manager(); MOZ_ASSERT(actor); return !BackgroundParent::IsOtherProcessActor(actor); } void NormalTransaction::SendCompleteNotification(nsresult aResult) { AssertIsOnBackgroundThread(); if (!IsActorDestroyed()) { Unused << SendComplete(aResult); } } void NormalTransaction::ActorDestroy(ActorDestroyReason aWhy) { AssertIsOnBackgroundThread(); NoteActorDestroyed(); if (!mCommittedOrAborted) { if (NS_SUCCEEDED(mResultCode)) { IDB_REPORT_INTERNAL_ERR(); mResultCode = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } mForceAborted.EnsureFlipped(); MaybeCommitOrAbort(); } } mozilla::ipc::IPCResult NormalTransaction::RecvDeleteMe() { AssertIsOnBackgroundThread(); MOZ_ASSERT(!IsActorDestroyed()); QM_WARNONLY_TRY(OkIf(PBackgroundIDBTransactionParent::Send__delete__(this))); return IPC_OK(); } mozilla::ipc::IPCResult NormalTransaction::RecvCommit( const Maybe& aLastRequest) { AssertIsOnBackgroundThread(); return TransactionBase::RecvCommit(this, aLastRequest); } mozilla::ipc::IPCResult NormalTransaction::RecvAbort( const nsresult& aResultCode) { AssertIsOnBackgroundThread(); return TransactionBase::RecvAbort(this, aResultCode); } PBackgroundIDBRequestParent* NormalTransaction::AllocPBackgroundIDBRequestParent( const RequestParams& aParams) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aParams.type() != RequestParams::T__None); return AllocRequest(std::move(const_cast(aParams)), IsSameProcessActor()); } mozilla::ipc::IPCResult NormalTransaction::RecvPBackgroundIDBRequestConstructor( PBackgroundIDBRequestParent* const aActor, const RequestParams& aParams) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); MOZ_ASSERT(aParams.type() != RequestParams::T__None); if (!StartRequest(aActor)) { return IPC_FAIL(this, "StartRequest failed!"); } return IPC_OK(); } bool NormalTransaction::DeallocPBackgroundIDBRequestParent( PBackgroundIDBRequestParent* const aActor) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); return DeallocRequest(aActor); } already_AddRefed NormalTransaction::AllocPBackgroundIDBCursorParent( const OpenCursorParams& aParams) { AssertIsOnBackgroundThread(); return AllocCursor(aParams, IsSameProcessActor()); } mozilla::ipc::IPCResult NormalTransaction::RecvPBackgroundIDBCursorConstructor( PBackgroundIDBCursorParent* const aActor, const OpenCursorParams& aParams) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); MOZ_ASSERT(aParams.type() != OpenCursorParams::T__None); if (!StartCursor(aActor, aParams)) { return IPC_FAIL(this, "StartCursor failed!"); } return IPC_OK(); } /******************************************************************************* * VersionChangeTransaction ******************************************************************************/ VersionChangeTransaction::VersionChangeTransaction( OpenDatabaseOp* aOpenDatabaseOp) : TransactionBase(aOpenDatabaseOp->mDatabase.clonePtr(), IDBTransaction::Mode::VersionChange), mOpenDatabaseOp(aOpenDatabaseOp) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aOpenDatabaseOp); } VersionChangeTransaction::~VersionChangeTransaction() { #ifdef DEBUG // Silence the base class' destructor assertion if we never made this actor // live. FakeActorDestroyed(); #endif } bool VersionChangeTransaction::IsSameProcessActor() { AssertIsOnBackgroundThread(); PBackgroundParent* actor = Manager()->Manager()->Manager(); MOZ_ASSERT(actor); return !BackgroundParent::IsOtherProcessActor(actor); } void VersionChangeTransaction::SetActorAlive() { AssertIsOnBackgroundThread(); MOZ_ASSERT(!IsActorDestroyed()); mActorWasAlive.Flip(); } bool VersionChangeTransaction::CopyDatabaseMetadata() { AssertIsOnBackgroundThread(); MOZ_ASSERT(!mOldMetadata); const auto& origMetadata = GetDatabase().Metadata(); SafeRefPtr newMetadata = origMetadata.Duplicate(); if (NS_WARN_IF(!newMetadata)) { return false; } // Replace the live metadata with the new mutable copy. DatabaseActorInfo* info; MOZ_ALWAYS_TRUE(gLiveDatabaseHashtable->Get(origMetadata.mDatabaseId, &info)); MOZ_ASSERT(!info->mLiveDatabases.IsEmpty()); MOZ_ASSERT(info->mMetadata == &origMetadata); mOldMetadata = std::move(info->mMetadata); info->mMetadata = std::move(newMetadata); // Replace metadata pointers for all live databases. for (const auto& liveDatabase : info->mLiveDatabases) { liveDatabase->mMetadata = info->mMetadata.clonePtr(); } return true; } void VersionChangeTransaction::UpdateMetadata(nsresult aResult) { AssertIsOnBackgroundThread(); MOZ_ASSERT(mOpenDatabaseOp); MOZ_ASSERT(!!mActorWasAlive == !!mOpenDatabaseOp->mDatabase); MOZ_ASSERT_IF(mActorWasAlive, !mOpenDatabaseOp->mDatabaseId.IsEmpty()); if (IsActorDestroyed() || !mActorWasAlive) { return; } SafeRefPtr oldMetadata = std::move(mOldMetadata); DatabaseActorInfo* info; if (!gLiveDatabaseHashtable->Get(oldMetadata->mDatabaseId, &info)) { return; } MOZ_ASSERT(!info->mLiveDatabases.IsEmpty()); if (NS_SUCCEEDED(aResult)) { // Remove all deleted objectStores and indexes, then mark immutable. info->mMetadata->mObjectStores.RemoveIf([](const auto& objectStoreIter) { MOZ_ASSERT(objectStoreIter.Key()); const SafeRefPtr& metadata = objectStoreIter.Data(); MOZ_ASSERT(metadata); if (metadata->mDeleted) { return true; } metadata->mIndexes.RemoveIf([](const auto& indexIter) -> bool { MOZ_ASSERT(indexIter.Key()); const SafeRefPtr& index = indexIter.Data(); MOZ_ASSERT(index); return index->mDeleted; }); metadata->mIndexes.MarkImmutable(); return false; }); info->mMetadata->mObjectStores.MarkImmutable(); } else { // Replace metadata pointers for all live databases. info->mMetadata = std::move(oldMetadata); for (auto& liveDatabase : info->mLiveDatabases) { liveDatabase->mMetadata = info->mMetadata.clonePtr(); } } } void VersionChangeTransaction::SendCompleteNotification(nsresult aResult) { AssertIsOnBackgroundThread(); MOZ_ASSERT(mOpenDatabaseOp); MOZ_ASSERT_IF(!mActorWasAlive, mOpenDatabaseOp->HasFailed()); MOZ_ASSERT_IF(!mActorWasAlive, mOpenDatabaseOp->mState > OpenDatabaseOp::State::SendingResults); const RefPtr openDatabaseOp = std::move(mOpenDatabaseOp); if (!mActorWasAlive) { return; } if (NS_FAILED(aResult)) { // 3.3.1 Opening a database: // "If the upgrade transaction was aborted, run the steps for closing a // database connection with connection, create and return a new AbortError // exception and abort these steps." openDatabaseOp->SetFailureCodeIfUnset(NS_ERROR_DOM_INDEXEDDB_ABORT_ERR); } openDatabaseOp->mState = OpenDatabaseOp::State::SendingResults; if (!IsActorDestroyed()) { Unused << SendComplete(aResult); } MOZ_ALWAYS_SUCCEEDS(openDatabaseOp->Run()); } void VersionChangeTransaction::ActorDestroy(ActorDestroyReason aWhy) { AssertIsOnBackgroundThread(); NoteActorDestroyed(); if (!mCommittedOrAborted) { if (NS_SUCCEEDED(mResultCode)) { IDB_REPORT_INTERNAL_ERR(); mResultCode = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } mForceAborted.EnsureFlipped(); MaybeCommitOrAbort(); } } mozilla::ipc::IPCResult VersionChangeTransaction::RecvDeleteMe() { AssertIsOnBackgroundThread(); MOZ_ASSERT(!IsActorDestroyed()); QM_WARNONLY_TRY( OkIf(PBackgroundIDBVersionChangeTransactionParent::Send__delete__(this))); return IPC_OK(); } mozilla::ipc::IPCResult VersionChangeTransaction::RecvCommit( const Maybe& aLastRequest) { AssertIsOnBackgroundThread(); return TransactionBase::RecvCommit(this, aLastRequest); } mozilla::ipc::IPCResult VersionChangeTransaction::RecvAbort( const nsresult& aResultCode) { AssertIsOnBackgroundThread(); return TransactionBase::RecvAbort(this, aResultCode); } mozilla::ipc::IPCResult VersionChangeTransaction::RecvCreateObjectStore( const ObjectStoreMetadata& aMetadata) { AssertIsOnBackgroundThread(); if (NS_WARN_IF(!aMetadata.id())) { return IPC_FAIL(this, "No metadata ID!"); } const SafeRefPtr dbMetadata = GetDatabase().MetadataPtr(); if (NS_WARN_IF(aMetadata.id() != dbMetadata->mNextObjectStoreId)) { return IPC_FAIL(this, "Requested metadata ID does not match next ID!"); } if (NS_WARN_IF( MatchMetadataNameOrId(dbMetadata->mObjectStores, aMetadata.id(), SomeRef(aMetadata.name())) .isSome())) { return IPC_FAIL(this, "MatchMetadataNameOrId failed!"); } if (NS_WARN_IF(mCommitOrAbortReceived)) { return IPC_FAIL(this, "Transaction is already committed/aborted!"); } const int64_t initialAutoIncrementId = aMetadata.autoIncrement() ? 1 : 0; auto newMetadata = MakeSafeRefPtr( aMetadata, FullObjectStoreMetadata::AutoIncrementIds{ initialAutoIncrementId, initialAutoIncrementId}); if (NS_WARN_IF(!dbMetadata->mObjectStores.InsertOrUpdate( aMetadata.id(), std::move(newMetadata), fallible))) { return IPC_FAIL(this, "mObjectStores.InsertOrUpdate failed!"); } dbMetadata->mNextObjectStoreId++; RefPtr op = new CreateObjectStoreOp( SafeRefPtrFromThis().downcast(), aMetadata); if (NS_WARN_IF(!op->Init(*this))) { op->Cleanup(); return IPC_FAIL(this, "ObjectStoreOp initialization failed!"); } op->DispatchToConnectionPool(); return IPC_OK(); } mozilla::ipc::IPCResult VersionChangeTransaction::RecvDeleteObjectStore( const IndexOrObjectStoreId& aObjectStoreId) { AssertIsOnBackgroundThread(); if (NS_WARN_IF(!aObjectStoreId)) { return IPC_FAIL(this, "No ObjectStoreId!"); } const auto& dbMetadata = GetDatabase().Metadata(); MOZ_ASSERT(dbMetadata.mNextObjectStoreId > 0); if (NS_WARN_IF(aObjectStoreId >= dbMetadata.mNextObjectStoreId)) { return IPC_FAIL(this, "Invalid ObjectStoreId!"); } SafeRefPtr foundMetadata = GetMetadataForObjectStoreId(aObjectStoreId); if (NS_WARN_IF(!foundMetadata)) { return IPC_FAIL(this, "No metadata found for ObjectStoreId!"); } if (NS_WARN_IF(mCommitOrAbortReceived)) { return IPC_FAIL(this, "Transaction is already committed/aborted!"); } foundMetadata->mDeleted.Flip(); DebugOnly foundTargetId = false; const bool isLastObjectStore = std::all_of( dbMetadata.mObjectStores.begin(), dbMetadata.mObjectStores.end(), [&foundTargetId, aObjectStoreId](const auto& objectStoreEntry) -> bool { if (uint64_t(aObjectStoreId) == objectStoreEntry.GetKey()) { foundTargetId = true; return true; } return objectStoreEntry.GetData()->mDeleted; }); MOZ_ASSERT_IF(isLastObjectStore, foundTargetId); RefPtr op = new DeleteObjectStoreOp( SafeRefPtrFromThis().downcast(), std::move(foundMetadata), isLastObjectStore); if (NS_WARN_IF(!op->Init(*this))) { op->Cleanup(); return IPC_FAIL(this, "ObjectStoreOp initialization failed!"); } op->DispatchToConnectionPool(); return IPC_OK(); } mozilla::ipc::IPCResult VersionChangeTransaction::RecvRenameObjectStore( const IndexOrObjectStoreId& aObjectStoreId, const nsAString& aName) { AssertIsOnBackgroundThread(); if (NS_WARN_IF(!aObjectStoreId)) { return IPC_FAIL(this, "No ObjectStoreId!"); } { const auto& dbMetadata = GetDatabase().Metadata(); MOZ_ASSERT(dbMetadata.mNextObjectStoreId > 0); if (NS_WARN_IF(aObjectStoreId >= dbMetadata.mNextObjectStoreId)) { return IPC_FAIL(this, "Invalid ObjectStoreId!"); } } SafeRefPtr foundMetadata = GetMetadataForObjectStoreId(aObjectStoreId); if (NS_WARN_IF(!foundMetadata)) { return IPC_FAIL(this, "No metadata found for ObjectStoreId!"); } if (NS_WARN_IF(mCommitOrAbortReceived)) { return IPC_FAIL(this, "Transaction is already committed/aborted!"); } foundMetadata->mCommonMetadata.name() = aName; RefPtr renameOp = new RenameObjectStoreOp( SafeRefPtrFromThis().downcast(), *foundMetadata); if (NS_WARN_IF(!renameOp->Init(*this))) { renameOp->Cleanup(); return IPC_FAIL(this, "ObjectStoreOp initialization failed!"); } renameOp->DispatchToConnectionPool(); return IPC_OK(); } mozilla::ipc::IPCResult VersionChangeTransaction::RecvCreateIndex( const IndexOrObjectStoreId& aObjectStoreId, const IndexMetadata& aMetadata) { AssertIsOnBackgroundThread(); if (NS_WARN_IF(!aObjectStoreId)) { return IPC_FAIL(this, "No ObjectStoreId!"); } if (NS_WARN_IF(!aMetadata.id())) { return IPC_FAIL(this, "No Metadata id!"); } const auto dbMetadata = GetDatabase().MetadataPtr(); if (NS_WARN_IF(aMetadata.id() != dbMetadata->mNextIndexId)) { return IPC_FAIL(this, "Requested metadata ID does not match next ID!"); } SafeRefPtr foundObjectStoreMetadata = GetMetadataForObjectStoreId(aObjectStoreId); if (NS_WARN_IF(!foundObjectStoreMetadata)) { return IPC_FAIL(this, "GetMetadataForObjectStoreId failed!"); } if (NS_WARN_IF(MatchMetadataNameOrId( foundObjectStoreMetadata->mIndexes, aMetadata.id(), SomeRef(aMetadata.name())) .isSome())) { return IPC_FAIL(this, "MatchMetadataNameOrId failed!"); } if (NS_WARN_IF(mCommitOrAbortReceived)) { return IPC_FAIL(this, "Transaction is already committed/aborted!"); } auto newMetadata = MakeSafeRefPtr(); newMetadata->mCommonMetadata = aMetadata; if (NS_WARN_IF(!foundObjectStoreMetadata->mIndexes.InsertOrUpdate( aMetadata.id(), std::move(newMetadata), fallible))) { return IPC_FAIL(this, "mIndexes.InsertOrUpdate failed!"); } dbMetadata->mNextIndexId++; RefPtr op = new CreateIndexOp( SafeRefPtrFromThis().downcast(), aObjectStoreId, aMetadata); if (NS_WARN_IF(!op->Init(*this))) { op->Cleanup(); return IPC_FAIL(this, "ObjectStoreOp initialization failed!"); } op->DispatchToConnectionPool(); return IPC_OK(); } mozilla::ipc::IPCResult VersionChangeTransaction::RecvDeleteIndex( const IndexOrObjectStoreId& aObjectStoreId, const IndexOrObjectStoreId& aIndexId) { AssertIsOnBackgroundThread(); if (NS_WARN_IF(!aObjectStoreId)) { return IPC_FAIL(this, "No ObjectStoreId!"); } if (NS_WARN_IF(!aIndexId)) { return IPC_FAIL(this, "No Index id!"); } { const auto& dbMetadata = GetDatabase().Metadata(); MOZ_ASSERT(dbMetadata.mNextObjectStoreId > 0); MOZ_ASSERT(dbMetadata.mNextIndexId > 0); if (NS_WARN_IF(aObjectStoreId >= dbMetadata.mNextObjectStoreId)) { return IPC_FAIL(this, "Requested ObjectStoreId does not match next ID!"); } if (NS_WARN_IF(aIndexId >= dbMetadata.mNextIndexId)) { return IPC_FAIL(this, "Requested IndexId does not match next ID!"); } } SafeRefPtr foundObjectStoreMetadata = GetMetadataForObjectStoreId(aObjectStoreId); if (NS_WARN_IF(!foundObjectStoreMetadata)) { return IPC_FAIL(this, "GetMetadataForObjectStoreId failed!"); } SafeRefPtr foundIndexMetadata = GetMetadataForIndexId(*foundObjectStoreMetadata, aIndexId); if (NS_WARN_IF(!foundIndexMetadata)) { return IPC_FAIL(this, "GetMetadataForIndexId failed!"); } if (NS_WARN_IF(mCommitOrAbortReceived)) { return IPC_FAIL(this, "Transaction is already committed/aborted!"); } foundIndexMetadata->mDeleted.Flip(); DebugOnly foundTargetId = false; const bool isLastIndex = std::all_of(foundObjectStoreMetadata->mIndexes.cbegin(), foundObjectStoreMetadata->mIndexes.cend(), [&foundTargetId, aIndexId](const auto& indexEntry) -> bool { if (uint64_t(aIndexId) == indexEntry.GetKey()) { foundTargetId = true; return true; } return indexEntry.GetData()->mDeleted; }); MOZ_ASSERT_IF(isLastIndex, foundTargetId); RefPtr op = new DeleteIndexOp( SafeRefPtrFromThis().downcast(), aObjectStoreId, aIndexId, foundIndexMetadata->mCommonMetadata.unique(), isLastIndex); if (NS_WARN_IF(!op->Init(*this))) { op->Cleanup(); return IPC_FAIL(this, "ObjectStoreOp initialization failed!"); } op->DispatchToConnectionPool(); return IPC_OK(); } mozilla::ipc::IPCResult VersionChangeTransaction::RecvRenameIndex( const IndexOrObjectStoreId& aObjectStoreId, const IndexOrObjectStoreId& aIndexId, const nsAString& aName) { AssertIsOnBackgroundThread(); if (NS_WARN_IF(!aObjectStoreId)) { return IPC_FAIL(this, "No ObjectStoreId!"); } if (NS_WARN_IF(!aIndexId)) { return IPC_FAIL(this, "No Index id!"); } const SafeRefPtr dbMetadata = GetDatabase().MetadataPtr(); MOZ_ASSERT(dbMetadata); MOZ_ASSERT(dbMetadata->mNextObjectStoreId > 0); MOZ_ASSERT(dbMetadata->mNextIndexId > 0); if (NS_WARN_IF(aObjectStoreId >= dbMetadata->mNextObjectStoreId)) { return IPC_FAIL(this, "Requested ObjectStoreId does not match next ID!"); } if (NS_WARN_IF(aIndexId >= dbMetadata->mNextIndexId)) { return IPC_FAIL(this, "Requested IndexId does not match next ID!"); } SafeRefPtr foundObjectStoreMetadata = GetMetadataForObjectStoreId(aObjectStoreId); if (NS_WARN_IF(!foundObjectStoreMetadata)) { return IPC_FAIL(this, "GetMetadataForObjectStoreId failed!"); } SafeRefPtr foundIndexMetadata = GetMetadataForIndexId(*foundObjectStoreMetadata, aIndexId); if (NS_WARN_IF(!foundIndexMetadata)) { return IPC_FAIL(this, "GetMetadataForIndexId failed!"); } if (NS_WARN_IF(mCommitOrAbortReceived)) { return IPC_FAIL(this, "Transaction is already committed/aborted!"); } foundIndexMetadata->mCommonMetadata.name() = aName; RefPtr renameOp = new RenameIndexOp( SafeRefPtrFromThis().downcast(), *foundIndexMetadata, aObjectStoreId); if (NS_WARN_IF(!renameOp->Init(*this))) { renameOp->Cleanup(); return IPC_FAIL(this, "ObjectStoreOp initialization failed!"); } renameOp->DispatchToConnectionPool(); return IPC_OK(); } PBackgroundIDBRequestParent* VersionChangeTransaction::AllocPBackgroundIDBRequestParent( const RequestParams& aParams) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aParams.type() != RequestParams::T__None); return AllocRequest(std::move(const_cast(aParams)), IsSameProcessActor()); } mozilla::ipc::IPCResult VersionChangeTransaction::RecvPBackgroundIDBRequestConstructor( PBackgroundIDBRequestParent* aActor, const RequestParams& aParams) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); MOZ_ASSERT(aParams.type() != RequestParams::T__None); if (!StartRequest(aActor)) { return IPC_FAIL(this, "StartRequest failed!"); } return IPC_OK(); } bool VersionChangeTransaction::DeallocPBackgroundIDBRequestParent( PBackgroundIDBRequestParent* aActor) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); return DeallocRequest(aActor); } already_AddRefed VersionChangeTransaction::AllocPBackgroundIDBCursorParent( const OpenCursorParams& aParams) { AssertIsOnBackgroundThread(); return AllocCursor(aParams, IsSameProcessActor()); } mozilla::ipc::IPCResult VersionChangeTransaction::RecvPBackgroundIDBCursorConstructor( PBackgroundIDBCursorParent* aActor, const OpenCursorParams& aParams) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); MOZ_ASSERT(aParams.type() != OpenCursorParams::T__None); if (!StartCursor(aActor, aParams)) { return IPC_FAIL(this, "StartCursor failed!"); } return IPC_OK(); } /******************************************************************************* * CursorBase ******************************************************************************/ CursorBase::CursorBase(SafeRefPtr aTransaction, SafeRefPtr aObjectStoreMetadata, const Direction aDirection, const ConstructFromTransactionBase /*aConstructionTag*/) : mTransaction(std::move(aTransaction)), mObjectStoreMetadata(WrapNotNull(std::move(aObjectStoreMetadata))), mObjectStoreId((*mObjectStoreMetadata)->mCommonMetadata.id()), mDirection(aDirection), mMaxExtraCount(IndexedDatabaseManager::MaxPreloadExtraRecords()), mIsSameProcessActor(!BackgroundParent::IsOtherProcessActor( mTransaction->GetBackgroundParent())) { AssertIsOnBackgroundThread(); MOZ_ASSERT(mTransaction); static_assert( OpenCursorParams::T__None == 0 && OpenCursorParams::T__Last == 4, "Lots of code here assumes only four types of cursors!"); } template bool Cursor::VerifyRequestParams( const CursorRequestParams& aParams, const CursorPosition& aPosition) const { AssertIsOnBackgroundThread(); MOZ_ASSERT(aParams.type() != CursorRequestParams::T__None); MOZ_ASSERT(this->mObjectStoreMetadata); if constexpr (IsIndexCursor) { MOZ_ASSERT(this->mIndexMetadata); } #ifdef DEBUG { const SafeRefPtr objectStoreMetadata = mTransaction->GetMetadataForObjectStoreId(mObjectStoreId); if (objectStoreMetadata) { MOZ_ASSERT(objectStoreMetadata == (*this->mObjectStoreMetadata)); } else { MOZ_ASSERT((*this->mObjectStoreMetadata)->mDeleted); } if constexpr (IsIndexCursor) { if (objectStoreMetadata) { const SafeRefPtr indexMetadata = mTransaction->GetMetadataForIndexId(*objectStoreMetadata, this->mIndexId); if (indexMetadata) { MOZ_ASSERT(indexMetadata == *this->mIndexMetadata); } else { MOZ_ASSERT((*this->mIndexMetadata)->mDeleted); } } } } #endif if (NS_AUUF_OR_WARN_IF((*this->mObjectStoreMetadata)->mDeleted)) { return false; } if constexpr (IsIndexCursor) { if (NS_AUUF_OR_WARN_IF(this->mIndexMetadata && (*this->mIndexMetadata)->mDeleted)) { return false; } } const Key& sortKey = aPosition.GetSortKey(this->IsLocaleAware()); switch (aParams.type()) { case CursorRequestParams::TContinueParams: { const Key& key = aParams.get_ContinueParams().key(); if (!key.IsUnset()) { switch (mDirection) { case IDBCursorDirection::Next: case IDBCursorDirection::Nextunique: if (NS_AUUF_OR_WARN_IF(key <= sortKey)) { return false; } break; case IDBCursorDirection::Prev: case IDBCursorDirection::Prevunique: if (NS_AUUF_OR_WARN_IF(key >= sortKey)) { return false; } break; default: MOZ_CRASH("Should never get here!"); } } break; } case CursorRequestParams::TContinuePrimaryKeyParams: { if constexpr (IsIndexCursor) { const Key& key = aParams.get_ContinuePrimaryKeyParams().key(); const Key& primaryKey = aParams.get_ContinuePrimaryKeyParams().primaryKey(); MOZ_ASSERT(!key.IsUnset()); MOZ_ASSERT(!primaryKey.IsUnset()); switch (mDirection) { case IDBCursorDirection::Next: if (NS_AUUF_OR_WARN_IF(key < sortKey || (key == sortKey && primaryKey <= aPosition.mObjectStoreKey))) { return false; } break; case IDBCursorDirection::Prev: if (NS_AUUF_OR_WARN_IF(key > sortKey || (key == sortKey && primaryKey >= aPosition.mObjectStoreKey))) { return false; } break; default: MOZ_CRASH("Should never get here!"); } } break; } case CursorRequestParams::TAdvanceParams: if (NS_AUUF_OR_WARN_IF(!aParams.get_AdvanceParams().count())) { return false; } break; default: MOZ_CRASH("Should never get here!"); } return true; } template bool Cursor::Start(const OpenCursorParams& aParams) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aParams.type() == ToOpenCursorParamsType(CursorType)); MOZ_ASSERT(this->mObjectStoreMetadata); if (NS_AUUF_OR_WARN_IF(mCurrentlyRunningOp)) { return false; } const Maybe& optionalKeyRange = GetCommonOpenCursorParams(aParams).optionalKeyRange(); const RefPtr openOp = new OpenOp(this, optionalKeyRange); if (NS_WARN_IF(!openOp->Init(*mTransaction))) { openOp->Cleanup(); return false; } openOp->DispatchToConnectionPool(); mCurrentlyRunningOp = openOp; return true; } void ValueCursorBase::ProcessFiles(CursorResponse& aResponse, const FilesArray& aFiles) { MOZ_ASSERT_IF( aResponse.type() == CursorResponse::Tnsresult || aResponse.type() == CursorResponse::Tvoid_t || aResponse.type() == CursorResponse::TArrayOfObjectStoreKeyCursorResponse || aResponse.type() == CursorResponse::TArrayOfIndexKeyCursorResponse, aFiles.IsEmpty()); for (size_t i = 0; i < aFiles.Length(); ++i) { const auto& files = aFiles[i]; if (!files.IsEmpty()) { // TODO: Replace this assertion by one that checks if the response type // matches the cursor type, at a more generic location. MOZ_ASSERT(aResponse.type() == CursorResponse::TArrayOfObjectStoreCursorResponse || aResponse.type() == CursorResponse::TArrayOfIndexCursorResponse); SerializedStructuredCloneReadInfo* serializedInfo = nullptr; switch (aResponse.type()) { case CursorResponse::TArrayOfObjectStoreCursorResponse: { auto& responses = aResponse.get_ArrayOfObjectStoreCursorResponse(); MOZ_ASSERT(i < responses.Length()); serializedInfo = &responses[i].cloneInfo(); break; } case CursorResponse::TArrayOfIndexCursorResponse: { auto& responses = aResponse.get_ArrayOfIndexCursorResponse(); MOZ_ASSERT(i < responses.Length()); serializedInfo = &responses[i].cloneInfo(); break; } default: MOZ_CRASH("Should never get here!"); } MOZ_ASSERT(serializedInfo); MOZ_ASSERT(serializedInfo->files().IsEmpty()); MOZ_ASSERT(this->mDatabase); QM_TRY_UNWRAP(serializedInfo->files(), SerializeStructuredCloneFiles(this->mDatabase, files, /* aForPreprocess */ false), QM_VOID, [&aResponse](const nsresult result) { aResponse = ClampResultCode(result); }); } } } template void Cursor::SendResponseInternal( CursorResponse& aResponse, const FilesArrayT& aFiles) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aResponse.type() != CursorResponse::T__None); MOZ_ASSERT_IF(aResponse.type() == CursorResponse::Tnsresult, NS_FAILED(aResponse.get_nsresult())); MOZ_ASSERT_IF(aResponse.type() == CursorResponse::Tnsresult, NS_ERROR_GET_MODULE(aResponse.get_nsresult()) == NS_ERROR_MODULE_DOM_INDEXEDDB); MOZ_ASSERT(this->mObjectStoreMetadata); MOZ_ASSERT(mCurrentlyRunningOp); KeyValueBase::ProcessFiles(aResponse, aFiles); // Work around the deleted function by casting to the base class. QM_WARNONLY_TRY(OkIf( static_cast(this)->SendResponse(aResponse))); mCurrentlyRunningOp = nullptr; } template void Cursor::ActorDestroy(ActorDestroyReason aWhy) { AssertIsOnBackgroundThread(); if (mCurrentlyRunningOp) { mCurrentlyRunningOp->NoteActorDestroyed(); } if constexpr (IsValueCursor) { this->mBackgroundParent.destroy(); } this->mObjectStoreMetadata.destroy(); if constexpr (IsIndexCursor) { this->mIndexMetadata.destroy(); } } template mozilla::ipc::IPCResult Cursor::RecvDeleteMe() { AssertIsOnBackgroundThread(); MOZ_ASSERT(this->mObjectStoreMetadata); if (NS_WARN_IF(mCurrentlyRunningOp)) { return IPC_FAIL( this, "Attempt to delete a cursor with a non-null mCurrentlyRunningOp!"); } QM_WARNONLY_TRY(OkIf(PBackgroundIDBCursorParent::Send__delete__(this))); return IPC_OK(); } template mozilla::ipc::IPCResult Cursor::RecvContinue( const CursorRequestParams& aParams, const Key& aCurrentKey, const Key& aCurrentObjectStoreKey) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aParams.type() != CursorRequestParams::T__None); MOZ_ASSERT(this->mObjectStoreMetadata); if constexpr (IsIndexCursor) { MOZ_ASSERT(this->mIndexMetadata); } const bool trustParams = #ifdef DEBUG // Always verify parameters in DEBUG builds! false #else this->mIsSameProcessActor #endif ; MOZ_ASSERT(!aCurrentKey.IsUnset()); QM_TRY_UNWRAP( auto position, ([&]() -> Result, mozilla::ipc::IPCResult> { if constexpr (IsIndexCursor) { auto localeAwarePosition = Key{}; if (this->IsLocaleAware()) { QM_TRY_UNWRAP( localeAwarePosition, aCurrentKey.ToLocaleAwareKey(this->mLocale), Err(IPC_FAIL(this, "aCurrentKey.ToLocaleAwareKey failed!"))); } return CursorPosition{aCurrentKey, localeAwarePosition, aCurrentObjectStoreKey}; } else { return CursorPosition{aCurrentKey}; } }())); if (!trustParams && !VerifyRequestParams(aParams, position)) { return IPC_FAIL(this, "VerifyRequestParams failed!"); } if (NS_WARN_IF(mCurrentlyRunningOp)) { return IPC_FAIL(this, "Cursor is CurrentlyRunningOp!"); } if (NS_WARN_IF(mTransaction->mCommitOrAbortReceived)) { return IPC_FAIL(this, "Transaction is already committed/aborted!"); } const RefPtr continueOp = new ContinueOp(this, aParams, std::move(position)); if (NS_WARN_IF(!continueOp->Init(*mTransaction))) { continueOp->Cleanup(); return IPC_FAIL(this, "ContinueOp initialization failed!"); } continueOp->DispatchToConnectionPool(); mCurrentlyRunningOp = continueOp; return IPC_OK(); } /******************************************************************************* * DatabaseFileManager ******************************************************************************/ DatabaseFileManager::MutexType DatabaseFileManager::sMutex; DatabaseFileManager::DatabaseFileManager( PersistenceType aPersistenceType, const quota::OriginMetadata& aOriginMetadata, const nsAString& aDatabaseName, const nsCString& aDatabaseID, bool aEnforcingQuota, bool aIsInPrivateBrowsingMode) : mPersistenceType(aPersistenceType), mOriginMetadata(aOriginMetadata), mDatabaseName(aDatabaseName), mDatabaseID(aDatabaseID), mCipherKeyManager( aIsInPrivateBrowsingMode ? new IndexedDBCipherKeyManager("IndexedDBCipherKeyManager") : nullptr), mEnforcingQuota(aEnforcingQuota), mIsInPrivateBrowsingMode(aIsInPrivateBrowsingMode) {} nsresult DatabaseFileManager::Init(nsIFile* aDirectory, mozIStorageConnection& aConnection) { AssertIsOnIOThread(); MOZ_ASSERT(aDirectory); { QM_TRY_INSPECT(const bool& existsAsDirectory, ExistsAsDirectory(*aDirectory)); if (!existsAsDirectory) { QM_TRY(MOZ_TO_RESULT(aDirectory->Create(nsIFile::DIRECTORY_TYPE, 0755))); } QM_TRY_UNWRAP(auto path, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( nsString, aDirectory, GetPath)); mDirectoryPath.init(std::move(path)); } QM_TRY_INSPECT(const auto& journalDirectory, CloneFileAndAppend(*aDirectory, kJournalDirectoryName)); // We don't care if it doesn't exist at all, but if it does exist, make sure // it's a directory. QM_TRY_INSPECT(const bool& existsAsDirectory, ExistsAsDirectory(*journalDirectory)); Unused << existsAsDirectory; { QM_TRY_UNWRAP(auto path, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( nsString, journalDirectory, GetPath)); mJournalDirectoryPath.init(std::move(path)); } QM_TRY_INSPECT(const auto& stmt, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( nsCOMPtr, aConnection, CreateStatement, "SELECT id, refcount FROM file"_ns)); QM_TRY( CollectWhileHasResult(*stmt, [this](auto& stmt) -> Result { QM_TRY_INSPECT(const int64_t& id, MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt64, 0)); QM_TRY_INSPECT(const int32_t& dbRefCnt, MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt32, 1)); // We put a raw pointer into the hash table, so the memory refcount will // be 0, but the dbRefCnt is non-zero, which will keep the // DatabaseFileInfo object alive. MOZ_ASSERT(dbRefCnt > 0); mFileInfos.InsertOrUpdate( id, MakeNotNull( FileInfoManagerGuard{}, SafeRefPtrFromThis(), id, static_cast(dbRefCnt))); mLastFileId = std::max(id, mLastFileId); return Ok{}; })); mInitialized.Flip(); return NS_OK; } nsCOMPtr DatabaseFileManager::GetDirectory() { if (!this->AssertValid()) { return nullptr; } return GetFileForPath(*mDirectoryPath); } nsCOMPtr DatabaseFileManager::GetCheckedDirectory() { auto directory = GetDirectory(); if (NS_WARN_IF(!directory)) { return nullptr; } DebugOnly exists; MOZ_ASSERT(NS_SUCCEEDED(directory->Exists(&exists))); MOZ_ASSERT(exists); DebugOnly isDirectory; MOZ_ASSERT(NS_SUCCEEDED(directory->IsDirectory(&isDirectory))); MOZ_ASSERT(isDirectory); return directory; } nsCOMPtr DatabaseFileManager::GetJournalDirectory() { if (!this->AssertValid()) { return nullptr; } return GetFileForPath(*mJournalDirectoryPath); } nsCOMPtr DatabaseFileManager::EnsureJournalDirectory() { // This can happen on the IO or on a transaction thread. MOZ_ASSERT(!NS_IsMainThread()); auto journalDirectory = GetFileForPath(*mJournalDirectoryPath); QM_TRY(OkIf(journalDirectory), nullptr); QM_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE_MEMBER(journalDirectory, Exists), nullptr); if (exists) { QM_TRY_INSPECT(const bool& isDirectory, MOZ_TO_RESULT_INVOKE_MEMBER(journalDirectory, IsDirectory), nullptr); QM_TRY(OkIf(isDirectory), nullptr); } else { QM_TRY( MOZ_TO_RESULT(journalDirectory->Create(nsIFile::DIRECTORY_TYPE, 0755)), nullptr); } return journalDirectory; } // static nsCOMPtr DatabaseFileManager::GetFileForId(nsIFile* aDirectory, int64_t aId) { MOZ_ASSERT(aDirectory); MOZ_ASSERT(aId > 0); QM_TRY_RETURN(CloneFileAndAppend(*aDirectory, IntToString(aId)), nullptr); } // static nsCOMPtr DatabaseFileManager::GetCheckedFileForId(nsIFile* aDirectory, int64_t aId) { auto file = GetFileForId(aDirectory, aId); if (NS_WARN_IF(!file)) { return nullptr; } DebugOnly exists; MOZ_ASSERT(NS_SUCCEEDED(file->Exists(&exists))); MOZ_ASSERT(exists); DebugOnly isFile; MOZ_ASSERT(NS_SUCCEEDED(file->IsFile(&isFile))); MOZ_ASSERT(isFile); return file; } // static nsresult DatabaseFileManager::InitDirectory(nsIFile& aDirectory, nsIFile& aDatabaseFile, const nsACString& aOrigin, uint32_t aTelemetryId) { AssertIsOnIOThread(); { QM_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE_MEMBER(aDirectory, Exists)); if (!exists) { return NS_OK; } QM_TRY_INSPECT(const bool& isDirectory, MOZ_TO_RESULT_INVOKE_MEMBER(aDirectory, IsDirectory)); QM_TRY(OkIf(isDirectory), NS_ERROR_FAILURE); } QM_TRY_INSPECT(const auto& journalDirectory, CloneFileAndAppend(aDirectory, kJournalDirectoryName)); QM_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE_MEMBER(journalDirectory, Exists)); if (exists) { QM_TRY_INSPECT(const bool& isDirectory, MOZ_TO_RESULT_INVOKE_MEMBER(journalDirectory, IsDirectory)); QM_TRY(OkIf(isDirectory), NS_ERROR_FAILURE); bool hasJournals = false; QM_TRY(CollectEachFile( *journalDirectory, [&hasJournals](const nsCOMPtr& file) -> Result { QM_TRY_INSPECT( const auto& leafName, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, file, GetLeafName)); nsresult rv; leafName.ToInteger64(&rv); if (NS_SUCCEEDED(rv)) { hasJournals = true; } else { UNKNOWN_FILE_WARNING(leafName); } return Ok{}; })); if (hasJournals) { QM_TRY_UNWRAP(const NotNull> connection, CreateStorageConnection( aDatabaseFile, aDirectory, VoidString(), aOrigin, /* aDirectoryLockId */ -1, aTelemetryId, Nothing{})); mozStorageTransaction transaction(connection.get(), false); QM_TRY(MOZ_TO_RESULT(transaction.Start())) QM_TRY(MOZ_TO_RESULT(connection->ExecuteSimpleSQL( "CREATE VIRTUAL TABLE fs USING filesystem;"_ns))); // The parameter names are not used, parameters are bound by index only // locally in the same function. QM_TRY_INSPECT( const auto& stmt, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( nsCOMPtr, *connection, CreateStatement, "SELECT name, (name IN (SELECT id FROM file)) FROM fs WHERE path = :path"_ns)); QM_TRY_INSPECT(const auto& path, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( nsString, journalDirectory, GetPath)); QM_TRY(MOZ_TO_RESULT(stmt->BindStringByIndex(0, path))); QM_TRY(CollectWhileHasResult( *stmt, [&aDirectory, &journalDirectory](auto& stmt) -> Result { nsString name; QM_TRY(MOZ_TO_RESULT(stmt.GetString(0, name))); nsresult rv; name.ToInteger64(&rv); if (NS_FAILED(rv)) { return Ok{}; } int32_t flag = stmt.AsInt32(1); if (!flag) { QM_TRY_INSPECT(const auto& file, CloneFileAndAppend(aDirectory, name)); if (NS_FAILED(file->Remove(false))) { NS_WARNING("Failed to remove orphaned file!"); } } QM_TRY_INSPECT(const auto& journalFile, CloneFileAndAppend(*journalDirectory, name)); if (NS_FAILED(journalFile->Remove(false))) { NS_WARNING("Failed to remove journal file!"); } return Ok{}; })); QM_TRY(MOZ_TO_RESULT(connection->ExecuteSimpleSQL("DROP TABLE fs;"_ns))); QM_TRY(MOZ_TO_RESULT(transaction.Commit())); } } return NS_OK; } // static Result DatabaseFileManager::GetUsage( nsIFile* aDirectory) { AssertIsOnIOThread(); MOZ_ASSERT(aDirectory); FileUsageType usage; QM_TRY(TraverseFiles( *aDirectory, // KnownDirEntryOp [&usage](nsIFile& file, const bool isDirectory) -> Result { if (isDirectory) { return Ok{}; } // Usually we only use QM_OR_ELSE_LOG_VERBOSE(_IF) with Remove and // NS_ERROR_FILE_NOT_FOUND check, but the file was found by a directory // traversal and ToInteger on the name succeeded, so it should be our // file and if the file disappears, the use of QM_OR_ELSE_WARN_IF is ok // here. QM_TRY_INSPECT(const auto& thisUsage, QM_OR_ELSE_WARN_IF( // Expression. MOZ_TO_RESULT_INVOKE_MEMBER(file, GetFileSize) .map([](const int64_t fileSize) { return FileUsageType(Some(uint64_t(fileSize))); }), // Predicate. ([](const nsresult rv) { return rv == NS_ERROR_FILE_NOT_FOUND; }), // Fallback. If the file does no longer exist, treat // it as 0-sized. ErrToDefaultOk)); usage += thisUsage; return Ok{}; }, // UnknownDirEntryOp [](nsIFile&, const bool) -> Result { return Ok{}; })); return usage; } nsresult DatabaseFileManager::SyncDeleteFile(const int64_t aId) { MOZ_ASSERT(!mFileInfos.Contains(aId)); if (!this->AssertValid()) { return NS_ERROR_UNEXPECTED; } const auto directory = GetDirectory(); QM_TRY(OkIf(directory), NS_ERROR_FAILURE); const auto journalDirectory = GetJournalDirectory(); QM_TRY(OkIf(journalDirectory), NS_ERROR_FAILURE); const nsCOMPtr file = GetFileForId(directory, aId); QM_TRY(OkIf(file), NS_ERROR_FAILURE); const nsCOMPtr journalFile = GetFileForId(journalDirectory, aId); QM_TRY(OkIf(journalFile), NS_ERROR_FAILURE); return SyncDeleteFile(*file, *journalFile); } nsresult DatabaseFileManager::SyncDeleteFile(nsIFile& aFile, nsIFile& aJournalFile) const { QuotaManager* const quotaManager = EnforcingQuota() ? QuotaManager::Get() : nullptr; MOZ_ASSERT_IF(EnforcingQuota(), quotaManager); QM_TRY(MOZ_TO_RESULT(DeleteFile(aFile, quotaManager, Type(), OriginMetadata(), Idempotency::No))); QM_TRY(MOZ_TO_RESULT(aJournalFile.Remove(false))); return NS_OK; } nsresult DatabaseFileManager::Invalidate() { if (mCipherKeyManager) { mCipherKeyManager->Invalidate(); } QM_TRY(MOZ_TO_RESULT(FileInfoManager::Invalidate())); return NS_OK; } /******************************************************************************* * QuotaClient ******************************************************************************/ QuotaClient* QuotaClient::sInstance = nullptr; QuotaClient::QuotaClient() : mDeleteTimer(NS_NewTimer()) { AssertIsOnBackgroundThread(); MOZ_ASSERT(!sInstance, "We expect this to be a singleton!"); MOZ_ASSERT(!gTelemetryIdMutex); // Always create this so that later access to gTelemetryIdHashtable can be // properly synchronized. gTelemetryIdMutex = new Mutex("IndexedDB gTelemetryIdMutex"); gStorageDatabaseNameMutex = new Mutex("IndexedDB gStorageDatabaseNameMutex"); sInstance = this; } QuotaClient::~QuotaClient() { AssertIsOnBackgroundThread(); MOZ_ASSERT(sInstance == this, "We expect this to be a singleton!"); MOZ_ASSERT(gTelemetryIdMutex); MOZ_ASSERT(!mMaintenanceThreadPool); // No one else should be able to touch gTelemetryIdHashtable now that the // QuotaClient has gone away. gTelemetryIdHashtable = nullptr; gTelemetryIdMutex = nullptr; gStorageDatabaseNameHashtable = nullptr; gStorageDatabaseNameMutex = nullptr; sInstance = nullptr; } nsresult QuotaClient::AsyncDeleteFile(DatabaseFileManager* aFileManager, int64_t aFileId) { AssertIsOnBackgroundThread(); if (IsShuttingDownOnBackgroundThread()) { // Whoops! We want to delete an IndexedDB disk-backed File but it's too late // to actually delete the file! This means we're going to "leak" the file // and leave it around when we shouldn't! (The file will stay around until // next storage initialization is triggered when the app is started again). // Fixing this is tracked by bug 1539377. return NS_OK; } MOZ_ASSERT(mDeleteTimer); MOZ_ALWAYS_SUCCEEDS(mDeleteTimer->Cancel()); QM_TRY(MOZ_TO_RESULT(mDeleteTimer->InitWithNamedFuncCallback( DeleteTimerCallback, this, kDeleteTimeoutMs, nsITimer::TYPE_ONE_SHOT, "dom::indexeddb::QuotaClient::AsyncDeleteFile"))); mPendingDeleteInfos.GetOrInsertNew(aFileManager)->AppendElement(aFileId); return NS_OK; } nsresult QuotaClient::FlushPendingFileDeletions() { AssertIsOnBackgroundThread(); QM_TRY(MOZ_TO_RESULT(mDeleteTimer->Cancel())); DeleteTimerCallback(mDeleteTimer, this); return NS_OK; } nsThreadPool* QuotaClient::GetOrCreateThreadPool() { AssertIsOnBackgroundThread(); MOZ_ASSERT(!IsShuttingDownOnBackgroundThread()); if (!mMaintenanceThreadPool) { RefPtr threadPool = new nsThreadPool(); // PR_GetNumberOfProcessors() can return -1 on error, so make sure we // don't set some huge number here. We add 2 in case some threads block on // the disk I/O. const uint32_t threadCount = std::max(int32_t(PR_GetNumberOfProcessors()), int32_t(1)) + 2; MOZ_ALWAYS_SUCCEEDS(threadPool->SetThreadLimit(threadCount)); // Don't keep more than one idle thread. MOZ_ALWAYS_SUCCEEDS(threadPool->SetIdleThreadLimit(1)); // Don't keep idle threads alive very long. MOZ_ALWAYS_SUCCEEDS(threadPool->SetIdleThreadTimeout(5 * PR_MSEC_PER_SEC)); MOZ_ALWAYS_SUCCEEDS(threadPool->SetName("IndexedDB Mnt"_ns)); mMaintenanceThreadPool = std::move(threadPool); } return mMaintenanceThreadPool; } mozilla::dom::quota::Client::Type QuotaClient::GetType() { return QuotaClient::IDB; } nsresult QuotaClient::UpgradeStorageFrom1_0To2_0(nsIFile* aDirectory) { AssertIsOnIOThread(); MOZ_ASSERT(aDirectory); QM_TRY_INSPECT((const auto& [subdirsToProcess, databaseFilenames]), GetDatabaseFilenames(*aDirectory, /* aCanceled */ AtomicBool{false})); QM_TRY(CollectEachInRange( subdirsToProcess, [&databaseFilenames = databaseFilenames, aDirectory](const nsAString& subdirName) -> Result { // If the directory has the correct suffix then it should exist in // databaseFilenames. nsDependentSubstring subdirNameBase; if (GetFilenameBase(subdirName, kFileManagerDirectoryNameSuffix, subdirNameBase)) { QM_WARNONLY_TRY(OkIf(databaseFilenames.Contains(subdirNameBase))); return Ok{}; } // The directory didn't have the right suffix but we might need to // rename it. Check to see if we have a database that references this // directory. QM_TRY_INSPECT( const auto& subdirNameWithSuffix, ([&databaseFilenames, &subdirName]() -> Result { if (databaseFilenames.Contains(subdirName)) { return nsAutoString{subdirName + kFileManagerDirectoryNameSuffix}; } // Windows doesn't allow a directory to end with a dot ('.'), so // we have to check that possibility here too. We do this on all // platforms, because the origin directory may have been created // on Windows and now accessed on different OS. const nsAutoString subdirNameWithDot = subdirName + u"."_ns; QM_TRY(OkIf(databaseFilenames.Contains(subdirNameWithDot)), Err(NotOk{})); return nsAutoString{subdirNameWithDot + kFileManagerDirectoryNameSuffix}; }()), Ok{}); // We do have a database that uses this subdir so we should rename it // now. QM_TRY_INSPECT(const auto& subdir, CloneFileAndAppend(*aDirectory, subdirName)); DebugOnly isDirectory; MOZ_ASSERT(NS_SUCCEEDED(subdir->IsDirectory(&isDirectory))); MOZ_ASSERT(isDirectory); // Check if the subdir with suffix already exists before renaming. QM_TRY_INSPECT(const auto& subdirWithSuffix, CloneFileAndAppend(*aDirectory, subdirNameWithSuffix)); QM_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE_MEMBER(subdirWithSuffix, Exists)); if (exists) { IDB_WARNING("Deleting old %s files directory!", NS_ConvertUTF16toUTF8(subdirName).get()); QM_TRY(MOZ_TO_RESULT(subdir->Remove(/* aRecursive */ true))); return Ok{}; } // Finally, rename the subdir. QM_TRY(MOZ_TO_RESULT(subdir->RenameTo(nullptr, subdirNameWithSuffix))); return Ok{}; })); return NS_OK; } nsresult QuotaClient::UpgradeStorageFrom2_1To2_2(nsIFile* aDirectory) { AssertIsOnIOThread(); MOZ_ASSERT(aDirectory); QM_TRY(CollectEachFile( *aDirectory, [](const nsCOMPtr& file) -> Result { QM_TRY_INSPECT(const auto& dirEntryKind, GetDirEntryKind(*file)); switch (dirEntryKind) { case nsIFileKind::ExistsAsDirectory: break; case nsIFileKind::ExistsAsFile: { QM_TRY_INSPECT( const auto& leafName, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, file, GetLeafName)); // It's reported that files ending with ".tmp" somehow live in the // indexedDB directories in Bug 1503883. Such files shouldn't exist // in the indexedDB directory so remove them in this upgrade. if (StringEndsWith(leafName, u".tmp"_ns)) { IDB_WARNING("Deleting unknown temporary file!"); QM_TRY(MOZ_TO_RESULT(file->Remove(false))); } break; } case nsIFileKind::DoesNotExist: // Ignore files that got removed externally while iterating. break; } return Ok{}; })); return NS_OK; } Result QuotaClient::InitOrigin( PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, const AtomicBool& aCanceled) { AssertIsOnIOThread(); QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(this, GetUsageForOriginInternal, aPersistenceType, aOriginMetadata, aCanceled, /* aInitializing*/ true)); } nsresult QuotaClient::InitOriginWithoutTracking( PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, const AtomicBool& aCanceled) { AssertIsOnIOThread(); return GetUsageForOriginInternal(aPersistenceType, aOriginMetadata, aCanceled, /* aInitializing*/ true, nullptr); } Result QuotaClient::GetUsageForOrigin( PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, const AtomicBool& aCanceled) { AssertIsOnIOThread(); QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(this, GetUsageForOriginInternal, aPersistenceType, aOriginMetadata, aCanceled, /* aInitializing*/ false)); } nsresult QuotaClient::GetUsageForOriginInternal( PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, const AtomicBool& aCanceled, const bool aInitializing, UsageInfo* aUsageInfo) { AssertIsOnIOThread(); MOZ_ASSERT(aOriginMetadata.mPersistenceType == aPersistenceType); QM_TRY_INSPECT(const nsCOMPtr& directory, GetDirectory(aOriginMetadata)); // We need to see if there are any files in the directory already. If they // are database files then we need to cleanup stored files (if it's needed) // and also get the usage. // XXX Can we avoid unwrapping into non-const variables here? (Only // databaseFilenames is currently modified below) QM_TRY_UNWRAP((auto [subdirsToProcess, databaseFilenames, obsoleteFilenames]), GetDatabaseFilenames( *directory, aCanceled)); if (aInitializing) { QM_TRY(CollectEachInRange( subdirsToProcess, [&directory, &obsoleteFilenames = obsoleteFilenames, &databaseFilenames = databaseFilenames, aPersistenceType, &aOriginMetadata]( const nsAString& subdirName) -> Result { // The directory must have the correct suffix. nsDependentSubstring subdirNameBase; QM_TRY(QM_OR_ELSE_WARN( // Expression. ([&subdirName, &subdirNameBase] { QM_TRY_RETURN(OkIf(GetFilenameBase( subdirName, kFileManagerDirectoryNameSuffix, subdirNameBase))); }()), // Fallback. ([&directory, &subdirName](const NotOk) -> Result { // If there is an unexpected directory in the idb // directory, trying to delete at first instead of // breaking the whole initialization. QM_TRY(MOZ_TO_RESULT( DeleteFilesNoQuota(directory, subdirName)), Err(NS_ERROR_UNEXPECTED)); return Ok{}; })), Ok{}); if (obsoleteFilenames.Contains(subdirNameBase)) { // If this fails, it probably means we are in a serious situation. // e.g. Filesystem corruption. Will handle this in bug 1521541. QM_TRY(MOZ_TO_RESULT(RemoveDatabaseFilesAndDirectory( *directory, subdirNameBase, nullptr, aPersistenceType, aOriginMetadata, u""_ns)), Err(NS_ERROR_UNEXPECTED)); databaseFilenames.Remove(subdirNameBase); return Ok{}; } // The directory base must exist in databaseFilenames. // If there is an unexpected directory in the idb directory, trying to // delete at first instead of breaking the whole initialization. // XXX This is still somewhat quirky. It would be nice to make it // clear that the warning handler is infallible, which would also // remove the need for the error type conversion. QM_WARNONLY_TRY(QM_OR_ELSE_WARN( // Expression. OkIf(databaseFilenames.Contains(subdirNameBase)) .mapErr([](const NotOk) { return NS_ERROR_FAILURE; }), // Fallback. ([&directory, &subdirName](const nsresult) -> Result { // XXX It seems if we really got here, we can fail the // MOZ_ASSERT(!quotaManager->IsTemporaryStorageInitializedInternal()); // assertion in DeleteFilesNoQuota. QM_TRY(MOZ_TO_RESULT(DeleteFilesNoQuota(directory, subdirName)), Err(NS_ERROR_UNEXPECTED)); return Ok{}; }))); return Ok{}; })); } for (const auto& databaseFilename : databaseFilenames) { if (aCanceled) { break; } QM_TRY_INSPECT( const auto& fmDirectory, CloneFileAndAppend(*directory, databaseFilename + kFileManagerDirectoryNameSuffix)); QM_TRY_INSPECT( const auto& databaseFile, CloneFileAndAppend(*directory, databaseFilename + kSQLiteSuffix)); if (aInitializing) { QM_TRY(MOZ_TO_RESULT(DatabaseFileManager::InitDirectory( *fmDirectory, *databaseFile, aOriginMetadata.mOrigin, TelemetryIdForFile(databaseFile)))); } if (aUsageInfo) { { QM_TRY_INSPECT(const int64_t& fileSize, MOZ_TO_RESULT_INVOKE_MEMBER(databaseFile, GetFileSize)); MOZ_ASSERT(fileSize >= 0); *aUsageInfo += DatabaseUsageType(Some(uint64_t(fileSize))); } { QM_TRY_INSPECT(const auto& walFile, CloneFileAndAppend(*directory, databaseFilename + kSQLiteWALSuffix)); // QM_OR_ELSE_WARN_IF is not used here since we just want to log // NS_ERROR_FILE_NOT_FOUND result and not spam the reports (the -wal // file doesn't have to exist). QM_TRY_INSPECT(const int64_t& walFileSize, QM_OR_ELSE_LOG_VERBOSE_IF( // Expression. MOZ_TO_RESULT_INVOKE_MEMBER(walFile, GetFileSize), // Predicate. ([](const nsresult rv) { return rv == NS_ERROR_FILE_NOT_FOUND; }), // Fallback. (ErrToOk<0, int64_t>))); MOZ_ASSERT(walFileSize >= 0); *aUsageInfo += DatabaseUsageType(Some(uint64_t(walFileSize))); } { QM_TRY_INSPECT(const auto& fileUsage, DatabaseFileManager::GetUsage(fmDirectory)); *aUsageInfo += fileUsage; } } } return NS_OK; } void QuotaClient::OnOriginClearCompleted(PersistenceType aPersistenceType, const nsACString& aOrigin) { AssertIsOnIOThread(); if (IndexedDatabaseManager* mgr = IndexedDatabaseManager::Get()) { mgr->InvalidateFileManagers(aPersistenceType, aOrigin); } } void QuotaClient::OnRepositoryClearCompleted(PersistenceType aPersistenceType) { AssertIsOnIOThread(); if (IndexedDatabaseManager* mgr = IndexedDatabaseManager::Get()) { mgr->InvalidateFileManagers(aPersistenceType); } } void QuotaClient::ReleaseIOThreadObjects() { AssertIsOnIOThread(); if (IndexedDatabaseManager* mgr = IndexedDatabaseManager::Get()) { mgr->InvalidateAllFileManagers(); } } void QuotaClient::AbortOperationsForLocks( const DirectoryLockIdTable& aDirectoryLockIds) { AssertIsOnBackgroundThread(); InvalidateLiveDatabasesMatching([&aDirectoryLockIds](const auto& database) { // If the database is registered in gLiveDatabaseHashtable then it must have // a directory lock. return IsLockForObjectContainedInLockTable(database, aDirectoryLockIds); }); } void QuotaClient::AbortOperationsForProcess(ContentParentId aContentParentId) { AssertIsOnBackgroundThread(); InvalidateLiveDatabasesMatching([&aContentParentId](const auto& database) { return database.IsOwnedByProcess(aContentParentId); }); } void QuotaClient::AbortAllOperations() { AssertIsOnBackgroundThread(); AbortAllMaintenances(); InvalidateLiveDatabasesMatching([](const auto&) { return true; }); } void QuotaClient::StartIdleMaintenance() { AssertIsOnBackgroundThread(); if (IsShuttingDownOnBackgroundThread()) { MOZ_ASSERT(false, "!IsShuttingDownOnBackgroundThread()"); return; } if (!mBackgroundThread) { mBackgroundThread = GetCurrentSerialEventTarget(); } mMaintenanceQueue.EmplaceBack(MakeRefPtr(this)); ProcessMaintenanceQueue(); } void QuotaClient::StopIdleMaintenance() { AssertIsOnBackgroundThread(); AbortAllMaintenances(); } void QuotaClient::InitiateShutdown() { AssertIsOnBackgroundThread(); AbortAllOperations(); } bool QuotaClient::IsShutdownCompleted() const { return (!gFactoryOps || gFactoryOps->IsEmpty()) && (!gLiveDatabaseHashtable || !gLiveDatabaseHashtable->Count()) && !mCurrentMaintenance; } void QuotaClient::ForceKillActors() { // Currently we don't implement force killing actors. } nsCString QuotaClient::GetShutdownStatus() const { AssertIsOnBackgroundThread(); nsCString data; if (gFactoryOps && !gFactoryOps->IsEmpty()) { data.Append("FactoryOperations: "_ns + IntToCString(static_cast(gFactoryOps->Length())) + " ("_ns); // XXX It might be confusing to remove duplicates here, as the actual list // won't match the count then. nsTHashSet ids; std::transform(gFactoryOps->cbegin(), gFactoryOps->cend(), MakeInserter(ids), [](const auto& factoryOp) { MOZ_ASSERT(factoryOp); nsCString id; factoryOp->Stringify(id); return id; }); StringJoinAppend(data, ", "_ns, ids); data.Append(")\n"); } if (gLiveDatabaseHashtable && gLiveDatabaseHashtable->Count()) { data.Append("LiveDatabases: "_ns + IntToCString(gLiveDatabaseHashtable->Count()) + " ("_ns); // XXX What's the purpose of adding these to a hashtable before joining them // to the string? (Maybe this used to be an ordered container before???) nsTHashSet ids; for (const auto& entry : gLiveDatabaseHashtable->Values()) { MOZ_ASSERT(entry); std::transform(entry->mLiveDatabases.cbegin(), entry->mLiveDatabases.cend(), MakeInserter(ids), [](const auto& database) { nsCString id; database->Stringify(id); return id; }); } StringJoinAppend(data, ", "_ns, ids); data.Append(")\n"); } if (mCurrentMaintenance) { data.Append("IdleMaintenance: 1 ("); mCurrentMaintenance->Stringify(data); data.Append(")\n"); } return data; } void QuotaClient::FinalizeShutdown() { RefPtr connectionPool = gConnectionPool.get(); if (connectionPool) { connectionPool->Shutdown(); gConnectionPool = nullptr; } if (mMaintenanceThreadPool) { mMaintenanceThreadPool->Shutdown(); mMaintenanceThreadPool = nullptr; } if (mDeleteTimer) { MOZ_ALWAYS_SUCCEEDS(mDeleteTimer->Cancel()); mDeleteTimer = nullptr; } } void QuotaClient::DeleteTimerCallback(nsITimer* aTimer, void* aClosure) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aTimer); auto* const self = static_cast(aClosure); MOZ_ASSERT(self); MOZ_ASSERT(self->mDeleteTimer); MOZ_ASSERT(SameCOMIdentity(self->mDeleteTimer, aTimer)); for (const auto& pendingDeleteInfoEntry : self->mPendingDeleteInfos) { const auto& key = pendingDeleteInfoEntry.GetKey(); const auto& value = pendingDeleteInfoEntry.GetData(); MOZ_ASSERT(!value->IsEmpty()); RefPtr runnable = new DeleteFilesRunnable( SafeRefPtr{key, AcquireStrongRefFromRawPtr{}}, std::move(*value)); MOZ_ASSERT(value->IsEmpty()); runnable->RunImmediately(); } self->mPendingDeleteInfos.Clear(); } void QuotaClient::AbortAllMaintenances() { if (mCurrentMaintenance) { mCurrentMaintenance->Abort(); } for (const auto& maintenance : mMaintenanceQueue) { maintenance->Abort(); } } Result, nsresult> QuotaClient::GetDirectory( const OriginMetadata& aOriginMetadata) { QuotaManager* const quotaManager = QuotaManager::Get(); NS_ASSERTION(quotaManager, "This should never fail!"); QM_TRY_INSPECT(const auto& directory, quotaManager->GetOriginDirectory(aOriginMetadata)); MOZ_ASSERT(directory); QM_TRY(MOZ_TO_RESULT( directory->Append(NS_LITERAL_STRING_FROM_CSTRING(IDB_DIRECTORY_NAME)))); return directory; } template Result, nsresult> QuotaClient::GetDatabaseFilenames(nsIFile& aDirectory, const AtomicBool& aCanceled) { AssertIsOnIOThread(); GetDatabaseFilenamesResult result; QM_TRY(CollectEachFileAtomicCancelable( aDirectory, aCanceled, [&result](const nsCOMPtr& file) -> Result { QM_TRY_INSPECT(const auto& leafName, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( nsString, file, GetLeafName)); QM_TRY_INSPECT(const auto& dirEntryKind, GetDirEntryKind(*file)); switch (dirEntryKind) { case nsIFileKind::ExistsAsDirectory: result.subdirsToProcess.AppendElement(leafName); break; case nsIFileKind::ExistsAsFile: { if constexpr (ObsoleteFilenames == ObsoleteFilenamesHandling::Include) { if (StringBeginsWith(leafName, kIdbDeletionMarkerFilePrefix)) { result.obsoleteFilenames.Insert( Substring(leafName, kIdbDeletionMarkerFilePrefix.Length())); break; } } // Skip OS metadata files. These files are only used in different // platforms, but the profile can be shared across different // operating systems, so we check it on all platforms. if (QuotaManager::IsOSMetadata(leafName)) { break; } // Skip files starting with ".". if (QuotaManager::IsDotFile(leafName)) { break; } // Skip SQLite temporary files. These files take up space on disk // but will be deleted as soon as the database is opened, so we // don't count them towards quota. if (StringEndsWith(leafName, kSQLiteJournalSuffix) || StringEndsWith(leafName, kSQLiteSHMSuffix)) { break; } // The SQLite WAL file does count towards quota, but it is handled // below once we find the actual database file. if (StringEndsWith(leafName, kSQLiteWALSuffix)) { break; } nsDependentSubstring leafNameBase; if (!GetFilenameBase(leafName, kSQLiteSuffix, leafNameBase)) { UNKNOWN_FILE_WARNING(leafName); break; } result.databaseFilenames.Insert(leafNameBase); break; } case nsIFileKind::DoesNotExist: // Ignore files that got removed externally while iterating. break; } return Ok{}; })); return result; } void QuotaClient::ProcessMaintenanceQueue() { AssertIsOnBackgroundThread(); if (mCurrentMaintenance || mMaintenanceQueue.IsEmpty()) { return; } mCurrentMaintenance = mMaintenanceQueue[0]; mMaintenanceQueue.RemoveElementAt(0); mCurrentMaintenance->RunImmediately(); } /******************************************************************************* * DeleteFilesRunnable ******************************************************************************/ DeleteFilesRunnable::DeleteFilesRunnable( SafeRefPtr aFileManager, nsTArray&& aFileIds) : Runnable("dom::indexeddb::DeleteFilesRunnable"), mOwningEventTarget(GetCurrentSerialEventTarget()), mFileManager(std::move(aFileManager)), mFileIds(std::move(aFileIds)), mState(State_Initial) {} void DeleteFilesRunnable::RunImmediately() { AssertIsOnBackgroundThread(); MOZ_ASSERT(mState == State_Initial); Unused << this->Run(); } void DeleteFilesRunnable::Open() { AssertIsOnBackgroundThread(); MOZ_ASSERT(mState == State_Initial); QuotaManager* const quotaManager = QuotaManager::Get(); if (NS_WARN_IF(!quotaManager)) { Finish(); return; } mState = State_DirectoryOpenPending; quotaManager ->OpenClientDirectory( {mFileManager->OriginMetadata(), quota::Client::IDB}) ->Then( GetCurrentSerialEventTarget(), __func__, [self = RefPtr(this)]( const ClientDirectoryLockPromise::ResolveOrRejectValue& aValue) { if (aValue.IsResolve()) { self->DirectoryLockAcquired(aValue.ResolveValue()); } else { self->DirectoryLockFailed(); } }); } void DeleteFilesRunnable::DoDatabaseWork() { AssertIsOnIOThread(); MOZ_ASSERT(mState == State_DatabaseWorkOpen); if (!mFileManager->Invalidated()) { for (int64_t fileId : mFileIds) { if (NS_FAILED(mFileManager->SyncDeleteFile(fileId))) { NS_WARNING("Failed to delete file!"); } } } Finish(); } void DeleteFilesRunnable::Finish() { MOZ_ASSERT(mState != State_UnblockingOpen); // Must set mState before dispatching otherwise we will race with the main // thread. mState = State_UnblockingOpen; MOZ_ALWAYS_SUCCEEDS(mOwningEventTarget->Dispatch(this, NS_DISPATCH_NORMAL)); } void DeleteFilesRunnable::UnblockOpen() { AssertIsOnBackgroundThread(); MOZ_ASSERT(mState == State_UnblockingOpen); mDirectoryLock = nullptr; mState = State_Completed; } NS_IMETHODIMP DeleteFilesRunnable::Run() { switch (mState) { case State_Initial: Open(); break; case State_DatabaseWorkOpen: DoDatabaseWork(); break; case State_UnblockingOpen: UnblockOpen(); break; case State_DirectoryOpenPending: default: MOZ_CRASH("Should never get here!"); } return NS_OK; } void DeleteFilesRunnable::DirectoryLockAcquired(DirectoryLock* aLock) { AssertIsOnBackgroundThread(); MOZ_ASSERT(mState == State_DirectoryOpenPending); MOZ_ASSERT(!mDirectoryLock); mDirectoryLock = aLock; QuotaManager* const quotaManager = QuotaManager::Get(); MOZ_ASSERT(quotaManager); // Must set this before dispatching otherwise we will race with the IO thread mState = State_DatabaseWorkOpen; QM_TRY(MOZ_TO_RESULT( quotaManager->IOThread()->Dispatch(this, NS_DISPATCH_NORMAL)), QM_VOID, [this](const nsresult) { Finish(); }); } void DeleteFilesRunnable::DirectoryLockFailed() { AssertIsOnBackgroundThread(); MOZ_ASSERT(mState == State_DirectoryOpenPending); MOZ_ASSERT(!mDirectoryLock); Finish(); } void Maintenance::Abort() { AssertIsOnBackgroundThread(); // Safe because mDatabaseMaintenances is modified // only in the background thread for (const auto& aDatabaseMaintenance : mDatabaseMaintenances) { aDatabaseMaintenance.GetData()->Abort(); } // mDirectoryLock must be cleared before transition to finished state mDirectoryLock = nullptr; mAborted = true; } void Maintenance::RegisterDatabaseMaintenance( DatabaseMaintenance* aDatabaseMaintenance) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aDatabaseMaintenance); MOZ_ASSERT(mState == State::BeginDatabaseMaintenance); MOZ_ASSERT( !mDatabaseMaintenances.Contains(aDatabaseMaintenance->DatabasePath())); mDatabaseMaintenances.InsertOrUpdate(aDatabaseMaintenance->DatabasePath(), aDatabaseMaintenance); } void Maintenance::UnregisterDatabaseMaintenance( DatabaseMaintenance* aDatabaseMaintenance) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aDatabaseMaintenance); MOZ_ASSERT(mState == State::WaitingForDatabaseMaintenancesToComplete); MOZ_ASSERT(mDatabaseMaintenances.Get(aDatabaseMaintenance->DatabasePath())); mDatabaseMaintenances.Remove(aDatabaseMaintenance->DatabasePath()); if (mDatabaseMaintenances.Count()) { return; } mState = State::Finishing; Finish(); } void Maintenance::Stringify(nsACString& aResult) const { AssertIsOnBackgroundThread(); aResult.Append("DatabaseMaintenances: "_ns + IntToCString(mDatabaseMaintenances.Count()) + " ("_ns); // XXX It might be confusing to remove duplicates here, as the actual list // won't match the count then. nsTHashSet ids; std::transform(mDatabaseMaintenances.Values().cbegin(), mDatabaseMaintenances.Values().cend(), MakeInserter(ids), [](const auto& entry) { MOZ_ASSERT(entry); nsCString id; entry->Stringify(id); return id; }); StringJoinAppend(aResult, ", "_ns, ids); aResult.Append(")"); } nsresult Maintenance::Start() { AssertIsOnBackgroundThread(); MOZ_ASSERT(mState == State::Initial); if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || IsAborted()) { return NS_ERROR_ABORT; } // Make sure that the IndexedDatabaseManager is running so that we can check // for low disk space mode. if (IndexedDatabaseManager::Get()) { OpenDirectory(); return NS_OK; } mState = State::CreateIndexedDatabaseManager; MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(this)); return NS_OK; } nsresult Maintenance::CreateIndexedDatabaseManager() { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mState == State::CreateIndexedDatabaseManager); if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || IsAborted()) { return NS_ERROR_ABORT; } IndexedDatabaseManager* const mgr = IndexedDatabaseManager::GetOrCreate(); if (NS_WARN_IF(!mgr)) { return NS_ERROR_FAILURE; } mState = State::IndexedDatabaseManagerOpen; MOZ_ALWAYS_SUCCEEDS( mQuotaClient->BackgroundThread()->Dispatch(this, NS_DISPATCH_NORMAL)); return NS_OK; } nsresult Maintenance::OpenDirectory() { AssertIsOnBackgroundThread(); MOZ_ASSERT(mState == State::Initial || mState == State::IndexedDatabaseManagerOpen); MOZ_ASSERT(!mDirectoryLock); MOZ_ASSERT(QuotaManager::Get()); if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || IsAborted()) { return NS_ERROR_ABORT; } QuotaManager* quotaManager = QuotaManager::Get(); MOZ_ASSERT(quotaManager); // Get a shared lock for /storage/*/*/idb mState = State::DirectoryOpenPending; quotaManager ->OpenStorageDirectory( Nullable(), OriginScope::FromNull(), Nullable(Client::IDB), /* aExclusive */ false, DirectoryLockCategory::None, SomeRef(mPendingDirectoryLock)) ->Then(GetCurrentSerialEventTarget(), __func__, [self = RefPtr(this)]( const UniversalDirectoryLockPromise::ResolveOrRejectValue& aValue) { if (aValue.IsResolve()) { self->DirectoryLockAcquired(aValue.ResolveValue()); } else { self->DirectoryLockFailed(); } }); return NS_OK; } nsresult Maintenance::DirectoryOpen() { AssertIsOnBackgroundThread(); MOZ_ASSERT(mState == State::DirectoryOpenPending); MOZ_ASSERT(mDirectoryLock); if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || IsAborted()) { return NS_ERROR_ABORT; } QuotaManager* const quotaManager = QuotaManager::Get(); MOZ_ASSERT(quotaManager); mState = State::DirectoryWorkOpen; QM_TRY(MOZ_TO_RESULT( quotaManager->IOThread()->Dispatch(this, NS_DISPATCH_NORMAL)), NS_ERROR_FAILURE); return NS_OK; } nsresult Maintenance::DirectoryWork() { AssertIsOnIOThread(); MOZ_ASSERT(mState == State::DirectoryWorkOpen); // The storage directory is structured like this: // // /storage///idb/*.sqlite // // We have to find all database files that match any persistence type and any // origin. We ignore anything out of the ordinary for now. if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || IsAborted()) { return NS_ERROR_ABORT; } QuotaManager* const quotaManager = QuotaManager::Get(); MOZ_ASSERT(quotaManager); // Since idle maintenance may occur before temporary storage is initialized, // make sure it's initialized here (all non-persistent origins need to be // cleaned up and quota info needs to be loaded for them). // Don't fail whole idle maintenance in case of an error, the persistent // repository can still // be processed. const bool initTemporaryStorageFailed = ["aManager] { QM_TRY(MOZ_TO_RESULT( quotaManager->EnsureTemporaryStorageIsInitializedInternal()), true); return false; }(); const nsCOMPtr storageDir = GetFileForPath(quotaManager->GetStoragePath()); QM_TRY(OkIf(storageDir), NS_ERROR_FAILURE); { QM_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE_MEMBER(storageDir, Exists)); // XXX No warning here? if (!exists) { return NS_ERROR_NOT_AVAILABLE; } } { QM_TRY_INSPECT(const bool& isDirectory, MOZ_TO_RESULT_INVOKE_MEMBER(storageDir, IsDirectory)); QM_TRY(OkIf(isDirectory), NS_ERROR_FAILURE); } // There are currently only 4 persistence types, and we want to iterate them // in this order: static const PersistenceType kPersistenceTypes[] = { PERSISTENCE_TYPE_PERSISTENT, PERSISTENCE_TYPE_DEFAULT, PERSISTENCE_TYPE_TEMPORARY, PERSISTENCE_TYPE_PRIVATE}; static_assert( ArrayLength(kPersistenceTypes) == size_t(PERSISTENCE_TYPE_INVALID), "Something changed with available persistence types!"); constexpr auto idbDirName = NS_LITERAL_STRING_FROM_CSTRING(IDB_DIRECTORY_NAME); for (const PersistenceType persistenceType : kPersistenceTypes) { // Loop over "" directories. if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || IsAborted()) { return NS_ERROR_ABORT; } // Don't do any maintenance for private browsing databases, which are only // temporary. if (persistenceType == PERSISTENCE_TYPE_PRIVATE) { continue; } const bool persistent = persistenceType == PERSISTENCE_TYPE_PERSISTENT; if (!persistent && initTemporaryStorageFailed) { // Non-persistent (best effort) repositories can't be processed if // temporary storage initialization failed. continue; } // XXX persistenceType == PERSISTENCE_TYPE_PERSISTENT shouldn't be a special // case... const auto persistenceTypeString = persistenceType == PERSISTENCE_TYPE_PERSISTENT ? "permanent"_ns : PersistenceTypeToString(persistenceType); QM_TRY_INSPECT(const auto& persistenceDir, CloneFileAndAppend(*storageDir, NS_ConvertASCIItoUTF16( persistenceTypeString))); { QM_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE_MEMBER(persistenceDir, Exists)); if (!exists) { continue; } QM_TRY_INSPECT(const bool& isDirectory, MOZ_TO_RESULT_INVOKE_MEMBER(persistenceDir, IsDirectory)); if (NS_WARN_IF(!isDirectory)) { continue; } } // Loop over "/idb" directories. QM_TRY(CollectEachFile( *persistenceDir, [this, "aManager, persistent, persistenceType, &idbDirName]( const nsCOMPtr& originDir) -> Result { if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || IsAborted()) { return Err(NS_ERROR_ABORT); } QM_TRY_INSPECT(const auto& dirEntryKind, GetDirEntryKind(*originDir)); switch (dirEntryKind) { case nsIFileKind::ExistsAsFile: break; case nsIFileKind::ExistsAsDirectory: { // Get the necessary information about the origin // (GetOriginMetadata also checks if it's a valid origin). QM_TRY_INSPECT(const auto& metadata, quotaManager->GetOriginMetadata(originDir), // Not much we can do here... Ok{}); // We now use a dedicated repository for private browsing // databases, but there could be some forgotten private browsing // databases in other repositories, so it's better to check for // that and don't do any maintenance for such databases. if (metadata.mIsPrivate) { return Ok{}; } if (persistent) { // We have to check that all persistent origins are cleaned up, // but there's no way to do that by one call, we need to // initialize (and possibly clean up) them one by one // (EnsureTemporaryStorageIsInitializedInternal cleans up only // non-persistent origins). QM_TRY_UNWRAP( const DebugOnly created, quotaManager->EnsurePersistentOriginIsInitialized(metadata) .map([](const auto& res) { return res.second; }), // Not much we can do here... Ok{}); // We found this origin directory by traversing the repository, // so EnsurePersistentOriginIsInitialized shouldn't report that // a new directory has been created. MOZ_ASSERT(!created); } QM_TRY_INSPECT(const auto& idbDir, CloneFileAndAppend(*originDir, idbDirName)); QM_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE_MEMBER(idbDir, Exists)); if (!exists) { return Ok{}; } QM_TRY_INSPECT(const bool& isDirectory, MOZ_TO_RESULT_INVOKE_MEMBER(idbDir, IsDirectory)); QM_TRY(OkIf(isDirectory), Ok{}); nsTArray databasePaths; // Loop over files in the "idb" directory. QM_TRY(CollectEachFile( *idbDir, [this, &databasePaths](const nsCOMPtr& idbDirFile) -> Result { if (NS_WARN_IF(QuotaClient:: IsShuttingDownOnNonBackgroundThread()) || IsAborted()) { return Err(NS_ERROR_ABORT); } QM_TRY_UNWRAP(auto idbFilePath, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( nsString, idbDirFile, GetPath)); if (!StringEndsWith(idbFilePath, kSQLiteSuffix)) { return Ok{}; } QM_TRY_INSPECT(const auto& dirEntryKind, GetDirEntryKind(*idbDirFile)); switch (dirEntryKind) { case nsIFileKind::ExistsAsDirectory: break; case nsIFileKind::ExistsAsFile: // Found a database. MOZ_ASSERT(!databasePaths.Contains(idbFilePath)); databasePaths.AppendElement(std::move(idbFilePath)); break; case nsIFileKind::DoesNotExist: // Ignore files that got removed externally while // iterating. break; } return Ok{}; })); if (!databasePaths.IsEmpty()) { mDirectoryInfos.EmplaceBack(persistenceType, metadata, std::move(databasePaths)); } break; } case nsIFileKind::DoesNotExist: // Ignore files that got removed externally while iterating. break; } return Ok{}; })); } mState = State::BeginDatabaseMaintenance; MOZ_ALWAYS_SUCCEEDS( mQuotaClient->BackgroundThread()->Dispatch(this, NS_DISPATCH_NORMAL)); return NS_OK; } nsresult Maintenance::BeginDatabaseMaintenance() { AssertIsOnBackgroundThread(); MOZ_ASSERT(mState == State::BeginDatabaseMaintenance); class MOZ_STACK_CLASS Helper final { public: static bool IsSafeToRunMaintenance(const nsAString& aDatabasePath) { if (gFactoryOps) { for (uint32_t index = gFactoryOps->Length(); index > 0; index--) { CheckedUnsafePtr& existingOp = (*gFactoryOps)[index - 1]; if (!existingOp->DatabaseFilePathIsKnown()) { continue; } if (existingOp->DatabaseFilePath() == aDatabasePath) { return false; } } } if (gLiveDatabaseHashtable) { return std::all_of( gLiveDatabaseHashtable->Values().cbegin(), gLiveDatabaseHashtable->Values().cend(), [&aDatabasePath](const auto& liveDatabasesEntry) { const auto& liveDatabases = liveDatabasesEntry->mLiveDatabases; return std::all_of(liveDatabases.cbegin(), liveDatabases.cend(), [&aDatabasePath](const auto& database) { return database->FilePath() != aDatabasePath; }); }); } return true; } }; if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || IsAborted()) { return NS_ERROR_ABORT; } RefPtr threadPool; for (DirectoryInfo& directoryInfo : mDirectoryInfos) { RefPtr directoryLock; for (const nsAString& databasePath : *directoryInfo.mDatabasePaths) { if (Helper::IsSafeToRunMaintenance(databasePath)) { if (!directoryLock) { directoryLock = mDirectoryLock->SpecializeForClient( directoryInfo.mPersistenceType, *directoryInfo.mOriginMetadata, Client::IDB); MOZ_ASSERT(directoryLock); } // No key needs to be passed here, because we skip encrypted databases // in DoDirectoryWork as long as they are only used in private browsing // mode. const auto databaseMaintenance = MakeRefPtr( this, directoryLock, directoryInfo.mPersistenceType, *directoryInfo.mOriginMetadata, databasePath, Nothing{}); if (!threadPool) { threadPool = mQuotaClient->GetOrCreateThreadPool(); MOZ_ASSERT(threadPool); } // Perform database maintenance on a TaskQueue, as database connections // require a serial event target when being opened in order to allow // memory pressure notifications to clear caches (bug 1806751). const auto taskQueue = TaskQueue::Create( do_AddRef(threadPool), "IndexedDB Database Maintenance"); MOZ_ALWAYS_SUCCEEDS( taskQueue->Dispatch(databaseMaintenance, NS_DISPATCH_NORMAL)); RegisterDatabaseMaintenance(databaseMaintenance); } } } mDirectoryInfos.Clear(); mDirectoryLock = nullptr; if (mDatabaseMaintenances.Count()) { mState = State::WaitingForDatabaseMaintenancesToComplete; } else { mState = State::Finishing; Finish(); } return NS_OK; } void Maintenance::Finish() { AssertIsOnBackgroundThread(); MOZ_ASSERT(!mDirectoryLock); MOZ_ASSERT(mState == State::Finishing); if (NS_FAILED(mResultCode)) { nsCString errorName; GetErrorName(mResultCode, errorName); IDB_WARNING("Maintenance finished with error: %s", errorName.get()); } // It can happen that we are only referenced by mCurrentMaintenance which is // cleared in NoteFinishedMaintenance() const RefPtr kungFuDeathGrip = this; mQuotaClient->NoteFinishedMaintenance(this); mState = State::Complete; } NS_IMETHODIMP Maintenance::Run() { MOZ_ASSERT(mState != State::Complete); const auto handleError = [this](const nsresult rv) { if (mState != State::Finishing) { if (NS_SUCCEEDED(mResultCode)) { mResultCode = rv; } // Must set mState before dispatching otherwise we will race with the // owning thread. mState = State::Finishing; if (IsOnBackgroundThread()) { Finish(); } else { MOZ_ALWAYS_SUCCEEDS(mQuotaClient->BackgroundThread()->Dispatch( this, NS_DISPATCH_NORMAL)); } } }; switch (mState) { case State::Initial: QM_TRY(MOZ_TO_RESULT(Start()), NS_OK, handleError); break; case State::CreateIndexedDatabaseManager: QM_TRY(MOZ_TO_RESULT(CreateIndexedDatabaseManager()), NS_OK, handleError); break; case State::IndexedDatabaseManagerOpen: QM_TRY(MOZ_TO_RESULT(OpenDirectory()), NS_OK, handleError); break; case State::DirectoryWorkOpen: QM_TRY(MOZ_TO_RESULT(DirectoryWork()), NS_OK, handleError); break; case State::BeginDatabaseMaintenance: QM_TRY(MOZ_TO_RESULT(BeginDatabaseMaintenance()), NS_OK, handleError); break; case State::Finishing: Finish(); break; default: MOZ_CRASH("Bad state!"); } return NS_OK; } void Maintenance::DirectoryLockAcquired(DirectoryLock* aLock) { AssertIsOnBackgroundThread(); MOZ_ASSERT(mState == State::DirectoryOpenPending); MOZ_ASSERT(!mDirectoryLock); mDirectoryLock = std::exchange(mPendingDirectoryLock, nullptr); nsresult rv = DirectoryOpen(); if (NS_WARN_IF(NS_FAILED(rv))) { if (NS_SUCCEEDED(mResultCode)) { mResultCode = rv; } mState = State::Finishing; Finish(); return; } } void Maintenance::DirectoryLockFailed() { AssertIsOnBackgroundThread(); MOZ_ASSERT(mState == State::DirectoryOpenPending); MOZ_ASSERT(!mDirectoryLock); mPendingDirectoryLock = nullptr; if (NS_SUCCEEDED(mResultCode)) { mResultCode = NS_ERROR_FAILURE; } mState = State::Finishing; Finish(); } void DatabaseMaintenance::Stringify(nsACString& aResult) const { AssertIsOnBackgroundThread(); aResult.AppendLiteral("Origin:"); aResult.Append(AnonymizedOriginString(mOriginMetadata.mOrigin)); aResult.Append(kQuotaGenericDelimiter); aResult.AppendLiteral("PersistenceType:"); aResult.Append(PersistenceTypeToString(mPersistenceType)); aResult.Append(kQuotaGenericDelimiter); aResult.AppendLiteral("Duration:"); aResult.AppendInt((PR_Now() - mMaintenance->StartTime()) / PR_USEC_PER_MSEC); } nsresult DatabaseMaintenance::Abort() { AssertIsOnBackgroundThread(); // StopIdleMaintenance and AbortAllOperations may request abort independently if (!mAborted.compareExchange(false, true)) { return NS_OK; } { auto shardStorageConnectionLocked = mSharedStorageConnection.Lock(); if (nsCOMPtr connection = *shardStorageConnectionLocked) { QM_TRY(MOZ_TO_RESULT(connection->Interrupt())); } } // mDirectoryLock must not be released here - otherwise QuotaVFS of storage // emits a crash to disallow getting a quota object for an unregistered // directory lock when connection is closed. // mDirectoryLock will be dropped by RunOnOwningThread in a timely fashion // after the interrupted maintenance completes. return NS_OK; } void DatabaseMaintenance::PerformMaintenanceOnDatabase() { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(mMaintenance); MOZ_ASSERT(mMaintenance->StartTime()); MOZ_ASSERT(mDirectoryLock); MOZ_ASSERT(!mDatabasePath.IsEmpty()); MOZ_ASSERT(!mOriginMetadata.mGroup.IsEmpty()); MOZ_ASSERT(!mOriginMetadata.mOrigin.IsEmpty()); if (NS_WARN_IF(IsAborted())) { return; } const nsCOMPtr databaseFile = GetFileForPath(mDatabasePath); MOZ_ASSERT(databaseFile); QM_TRY_UNWRAP( const NotNull> connection, GetStorageConnection(*databaseFile, mDirectoryLockId, TelemetryIdForFile(databaseFile), mMaybeKey), QM_VOID); auto autoClearConnection = MakeScopeExit([&]() { auto sharedStorageConnectionLocked = mSharedStorageConnection.Lock(); sharedStorageConnectionLocked.ref() = nullptr; connection->Close(); }); { auto sharedStorageConnectionLocked = mSharedStorageConnection.Lock(); sharedStorageConnectionLocked.ref() = connection; } auto databaseIsOk = false; QM_TRY(MOZ_TO_RESULT(CheckIntegrity(*connection, &databaseIsOk)), QM_VOID); QM_TRY(OkIf(databaseIsOk), QM_VOID, [](auto result) { // XXX Handle this somehow! Probably need to clear all storage for the // origin. See Bug 1760612. MOZ_ASSERT(false, "Database corruption detected!"); }); MaintenanceAction maintenanceAction; QM_TRY(MOZ_TO_RESULT(DetermineMaintenanceAction(*connection, databaseFile, &maintenanceAction)), QM_VOID); switch (maintenanceAction) { case MaintenanceAction::Nothing: break; case MaintenanceAction::IncrementalVacuum: IncrementalVacuum(*connection); break; case MaintenanceAction::FullVacuum: FullVacuum(*connection, databaseFile); break; default: MOZ_CRASH("Unknown MaintenanceAction!"); } } nsresult DatabaseMaintenance::CheckIntegrity(mozIStorageConnection& aConnection, bool* aOk) { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(aOk); if (NS_WARN_IF(IsAborted())) { return NS_ERROR_ABORT; } // First do a full integrity_check. Scope statements tightly here because // later operations require zero live statements. { QM_TRY_INSPECT(const auto& stmt, CreateAndExecuteSingleStepStatement( aConnection, "PRAGMA integrity_check(1);"_ns)); QM_TRY_INSPECT(const auto& result, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( nsString, *stmt, GetString, 0)); QM_TRY(OkIf(result.EqualsLiteral("ok")), NS_OK, [&aOk](const auto) { *aOk = false; }); } // Now enable and check for foreign key constraints. { QM_TRY_INSPECT( const int32_t& foreignKeysWereEnabled, ([&aConnection]() -> Result { QM_TRY_INSPECT(const auto& stmt, CreateAndExecuteSingleStepStatement( aConnection, "PRAGMA foreign_keys;"_ns)); QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(*stmt, GetInt32, 0)); }())); if (!foreignKeysWereEnabled) { QM_TRY(MOZ_TO_RESULT( aConnection.ExecuteSimpleSQL("PRAGMA foreign_keys = ON;"_ns))); } QM_TRY_INSPECT(const bool& foreignKeyError, CreateAndExecuteSingleStepStatement< SingleStepResult::ReturnNullIfNoResult>( aConnection, "PRAGMA foreign_key_check;"_ns)); if (!foreignKeysWereEnabled) { QM_TRY(MOZ_TO_RESULT( aConnection.ExecuteSimpleSQL("PRAGMA foreign_keys = OFF;"_ns))); } if (foreignKeyError) { *aOk = false; return NS_OK; } } *aOk = true; return NS_OK; } nsresult DatabaseMaintenance::DetermineMaintenanceAction( mozIStorageConnection& aConnection, nsIFile* aDatabaseFile, MaintenanceAction* aMaintenanceAction) { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(aDatabaseFile); MOZ_ASSERT(aMaintenanceAction); if (NS_WARN_IF(IsAborted())) { return NS_ERROR_ABORT; } QM_TRY_INSPECT(const int32_t& schemaVersion, MOZ_TO_RESULT_INVOKE_MEMBER(aConnection, GetSchemaVersion)); // Don't do anything if the schema version is less than 18; before that // version no databases had |auto_vacuum == INCREMENTAL| set and we didn't // track the values needed for the heuristics below. if (schemaVersion < MakeSchemaVersion(18, 0)) { *aMaintenanceAction = MaintenanceAction::Nothing; return NS_OK; } // This method shouldn't make any permanent changes to the database, so make // sure everything gets rolled back when we leave. mozStorageTransaction transaction(&aConnection, /* aCommitOnComplete */ false); QM_TRY(MOZ_TO_RESULT(transaction.Start())) // Check to see when we last vacuumed this database. QM_TRY_INSPECT(const auto& stmt, CreateAndExecuteSingleStepStatement( aConnection, "SELECT last_vacuum_time, last_vacuum_size " "FROM database;"_ns)); QM_TRY_INSPECT(const PRTime& lastVacuumTime, MOZ_TO_RESULT_INVOKE_MEMBER(*stmt, GetInt64, 0)); QM_TRY_INSPECT(const int64_t& lastVacuumSize, MOZ_TO_RESULT_INVOKE_MEMBER(*stmt, GetInt64, 1)); NS_ASSERTION(lastVacuumSize > 0, "Thy last vacuum size shall be greater than zero, less than " "zero shall thy last vacuum size not be. Zero is right out."); const PRTime startTime = mMaintenance->StartTime(); // This shouldn't really be possible... if (NS_WARN_IF(startTime <= lastVacuumTime)) { *aMaintenanceAction = MaintenanceAction::Nothing; return NS_OK; } if (startTime - lastVacuumTime < kMinVacuumAge) { *aMaintenanceAction = MaintenanceAction::IncrementalVacuum; return NS_OK; } // It has been more than a week since the database was vacuumed, so gather // statistics on its usage to see if vacuuming is worthwhile. // Create a temporary copy of the dbstat table to speed up the queries that // come later. QM_TRY(MOZ_TO_RESULT(aConnection.ExecuteSimpleSQL( "CREATE VIRTUAL TABLE __stats__ USING dbstat;" "CREATE TEMP TABLE __temp_stats__ AS SELECT * FROM __stats__;"_ns))); { // Calculate the percentage of the database pages that are not in // contiguous order. QM_TRY_INSPECT( const auto& stmt, CreateAndExecuteSingleStepStatement( aConnection, "SELECT SUM(__ts1__.pageno != __ts2__.pageno + 1) * 100.0 / " "COUNT(*) " "FROM __temp_stats__ AS __ts1__, __temp_stats__ AS __ts2__ " "WHERE __ts1__.name = __ts2__.name " "AND __ts1__.rowid = __ts2__.rowid + 1;"_ns)); QM_TRY_INSPECT(const int32_t& percentUnordered, MOZ_TO_RESULT_INVOKE_MEMBER(*stmt, GetInt32, 0)); MOZ_ASSERT(percentUnordered >= 0); MOZ_ASSERT(percentUnordered <= 100); if (percentUnordered >= kPercentUnorderedThreshold) { *aMaintenanceAction = MaintenanceAction::FullVacuum; return NS_OK; } } // Don't try a full vacuum if the file hasn't grown by 10%. QM_TRY_INSPECT(const int64_t& currentFileSize, MOZ_TO_RESULT_INVOKE_MEMBER(aDatabaseFile, GetFileSize)); if (currentFileSize <= lastVacuumSize || (((currentFileSize - lastVacuumSize) * 100 / currentFileSize) < kPercentFileSizeGrowthThreshold)) { *aMaintenanceAction = MaintenanceAction::IncrementalVacuum; return NS_OK; } { // See if there are any free pages that we can reclaim. QM_TRY_INSPECT(const auto& stmt, CreateAndExecuteSingleStepStatement( aConnection, "PRAGMA freelist_count;"_ns)); QM_TRY_INSPECT(const int32_t& freelistCount, MOZ_TO_RESULT_INVOKE_MEMBER(*stmt, GetInt32, 0)); MOZ_ASSERT(freelistCount >= 0); // If we have too many free pages then we should try an incremental // vacuum. If that causes too much fragmentation then we'll try a full // vacuum later. if (freelistCount > kMaxFreelistThreshold) { *aMaintenanceAction = MaintenanceAction::IncrementalVacuum; return NS_OK; } } { // Calculate the percentage of unused bytes on pages in the database. QM_TRY_INSPECT( const auto& stmt, CreateAndExecuteSingleStepStatement( aConnection, "SELECT SUM(unused) * 100.0 / SUM(pgsize) FROM __temp_stats__;"_ns)); QM_TRY_INSPECT(const int32_t& percentUnused, MOZ_TO_RESULT_INVOKE_MEMBER(*stmt, GetInt32, 0)); MOZ_ASSERT(percentUnused >= 0); MOZ_ASSERT(percentUnused <= 100); *aMaintenanceAction = percentUnused >= kPercentUnusedThreshold ? MaintenanceAction::FullVacuum : MaintenanceAction::IncrementalVacuum; } return NS_OK; } void DatabaseMaintenance::IncrementalVacuum( mozIStorageConnection& aConnection) { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!IsOnBackgroundThread()); if (NS_WARN_IF(IsAborted())) { return; } nsresult rv = aConnection.ExecuteSimpleSQL("PRAGMA incremental_vacuum;"_ns); if (NS_WARN_IF(NS_FAILED(rv))) { return; } } void DatabaseMaintenance::FullVacuum(mozIStorageConnection& aConnection, nsIFile* aDatabaseFile) { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(aDatabaseFile); if (NS_WARN_IF(IsAborted())) { return; } QM_WARNONLY_TRY(([&]() -> Result { QM_TRY(MOZ_TO_RESULT(aConnection.ExecuteSimpleSQL("VACUUM;"_ns))); const PRTime vacuumTime = PR_Now(); MOZ_ASSERT(vacuumTime > 0); QM_TRY_INSPECT(const int64_t& fileSize, MOZ_TO_RESULT_INVOKE_MEMBER(aDatabaseFile, GetFileSize)); MOZ_ASSERT(fileSize > 0); // The parameter names are not used, parameters are bound by index only // locally in the same function. QM_TRY_INSPECT(const auto& stmt, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( nsCOMPtr, aConnection, CreateStatement, "UPDATE database " "SET last_vacuum_time = :time" ", last_vacuum_size = :size;"_ns)); QM_TRY(MOZ_TO_RESULT(stmt->BindInt64ByIndex(0, vacuumTime))); QM_TRY(MOZ_TO_RESULT(stmt->BindInt64ByIndex(1, fileSize))); QM_TRY(MOZ_TO_RESULT(stmt->Execute())); return Ok{}; }())); } void DatabaseMaintenance::RunOnOwningThread() { AssertIsOnBackgroundThread(); mDirectoryLock = nullptr; if (mCompleteCallback) { MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(mCompleteCallback.forget())); } mMaintenance->UnregisterDatabaseMaintenance(this); } void DatabaseMaintenance::RunOnConnectionThread() { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!IsOnBackgroundThread()); PerformMaintenanceOnDatabase(); MOZ_ALWAYS_SUCCEEDS( mMaintenance->BackgroundThread()->Dispatch(this, NS_DISPATCH_NORMAL)); } NS_IMETHODIMP DatabaseMaintenance::Run() { if (IsOnBackgroundThread()) { RunOnOwningThread(); } else { RunOnConnectionThread(); } return NS_OK; } /******************************************************************************* * Local class implementations ******************************************************************************/ // static nsAutoCString DatabaseOperationBase::MaybeGetBindingClauseForKeyRange( const Maybe& aOptionalKeyRange, const nsACString& aKeyColumnName) { return aOptionalKeyRange.isSome() ? GetBindingClauseForKeyRange(aOptionalKeyRange.ref(), aKeyColumnName) : nsAutoCString{}; } // static nsAutoCString DatabaseOperationBase::GetBindingClauseForKeyRange( const SerializedKeyRange& aKeyRange, const nsACString& aKeyColumnName) { MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(!aKeyColumnName.IsEmpty()); constexpr auto andStr = " AND "_ns; constexpr auto spacecolon = " :"_ns; nsAutoCString result; if (aKeyRange.isOnly()) { // Both keys equal. result = andStr + aKeyColumnName + " ="_ns + spacecolon + kStmtParamNameLowerKey; } else { if (!aKeyRange.lower().IsUnset()) { // Lower key is set. result.Append(andStr + aKeyColumnName); result.AppendLiteral(" >"); if (!aKeyRange.lowerOpen()) { result.AppendLiteral("="); } result.Append(spacecolon + kStmtParamNameLowerKey); } if (!aKeyRange.upper().IsUnset()) { // Upper key is set. result.Append(andStr + aKeyColumnName); result.AppendLiteral(" <"); if (!aKeyRange.upperOpen()) { result.AppendLiteral("="); } result.Append(spacecolon + kStmtParamNameUpperKey); } } MOZ_ASSERT(!result.IsEmpty()); return result; } // static uint64_t DatabaseOperationBase::ReinterpretDoubleAsUInt64(double aDouble) { // This is a duplicate of the js engine's byte munging in StructuredClone.cpp return BitwiseCast(aDouble); } // static template nsresult DatabaseOperationBase::MaybeBindKeyToStatement( const Key& aKey, mozIStorageStatement* const aStatement, const nsACString& aParameterName, const KeyTransformation& aKeyTransformation) { MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(aStatement); if (!aKey.IsUnset()) { // XXX This case distinction could be avoided if QM_TRY_INSPECT would also // work with a function not returning a Result but simply a V (which // is const Key& here) and then assuming it is always a success. Or the // transformation could be changed to return Result but I // don't think that Result supports that at the moment. if constexpr (std::is_reference_v< std::invoke_result_t>) { QM_TRY(MOZ_TO_RESULT(aKeyTransformation(aKey).BindToStatement( aStatement, aParameterName))); } else { QM_TRY_INSPECT(const auto& transformedKey, aKeyTransformation(aKey)); QM_TRY(MOZ_TO_RESULT( transformedKey.BindToStatement(aStatement, aParameterName))); } } return NS_OK; } // static template nsresult DatabaseOperationBase::BindTransformedKeyRangeToStatement( const SerializedKeyRange& aKeyRange, mozIStorageStatement* const aStatement, const KeyTransformation& aKeyTransformation) { MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(aStatement); QM_TRY(MOZ_TO_RESULT(MaybeBindKeyToStatement(aKeyRange.lower(), aStatement, kStmtParamNameLowerKey, aKeyTransformation))); if (aKeyRange.isOnly()) { return NS_OK; } QM_TRY(MOZ_TO_RESULT(MaybeBindKeyToStatement(aKeyRange.upper(), aStatement, kStmtParamNameUpperKey, aKeyTransformation))); return NS_OK; } // static nsresult DatabaseOperationBase::BindKeyRangeToStatement( const SerializedKeyRange& aKeyRange, mozIStorageStatement* const aStatement) { return BindTransformedKeyRangeToStatement( aKeyRange, aStatement, [](const Key& key) -> const auto& { return key; }); } // static nsresult DatabaseOperationBase::BindKeyRangeToStatement( const SerializedKeyRange& aKeyRange, mozIStorageStatement* const aStatement, const nsCString& aLocale) { MOZ_ASSERT(!aLocale.IsEmpty()); return BindTransformedKeyRangeToStatement( aKeyRange, aStatement, [&aLocale](const Key& key) { return key.ToLocaleAwareKey(aLocale); }); } // static void CommonOpenOpHelperBase::AppendConditionClause( const nsACString& aColumnName, const nsACString& aStatementParameterName, bool aLessThan, bool aEquals, nsCString& aResult) { aResult += " AND "_ns + aColumnName + " "_ns; if (aLessThan) { aResult.Append('<'); } else { aResult.Append('>'); } if (aEquals) { aResult.Append('='); } aResult += " :"_ns + aStatementParameterName; } // static Result DatabaseOperationBase::IndexDataValuesFromUpdateInfos( const nsTArray& aUpdateInfos, const UniqueIndexTable& aUniqueIndexTable) { MOZ_ASSERT_IF(!aUpdateInfos.IsEmpty(), aUniqueIndexTable.Count()); AUTO_PROFILER_LABEL("DatabaseOperationBase::IndexDataValuesFromUpdateInfos", DOM); // XXX We could use TransformIntoNewArray here if it allowed to specify that // an AutoArray should be created. IndexDataValuesAutoArray indexValues; if (NS_WARN_IF(!indexValues.SetCapacity(aUpdateInfos.Length(), fallible))) { IDB_REPORT_INTERNAL_ERR(); return Err(NS_ERROR_OUT_OF_MEMORY); } std::transform(aUpdateInfos.cbegin(), aUpdateInfos.cend(), MakeBackInserter(indexValues), [&aUniqueIndexTable](const IndexUpdateInfo& updateInfo) { const IndexOrObjectStoreId& indexId = updateInfo.indexId(); bool unique = false; MOZ_ALWAYS_TRUE(aUniqueIndexTable.Get(indexId, &unique)); return IndexDataValue{indexId, unique, updateInfo.value(), updateInfo.localizedValue()}; }); indexValues.Sort(); return indexValues; } // static nsresult DatabaseOperationBase::InsertIndexTableRows( DatabaseConnection* aConnection, const IndexOrObjectStoreId aObjectStoreId, const Key& aObjectStoreKey, const nsTArray& aIndexValues) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(!aObjectStoreKey.IsUnset()); AUTO_PROFILER_LABEL("DatabaseOperationBase::InsertIndexTableRows", DOM); const uint32_t count = aIndexValues.Length(); if (!count) { return NS_OK; } auto insertUniqueStmt = DatabaseConnection::LazyStatement{ *aConnection, "INSERT INTO unique_index_data " "(index_id, value, object_store_id, " "object_data_key, value_locale) " "VALUES (:"_ns + kStmtParamNameIndexId + ", :"_ns + kStmtParamNameValue + ", :"_ns + kStmtParamNameObjectStoreId + ", :"_ns + kStmtParamNameObjectDataKey + ", :"_ns + kStmtParamNameValueLocale + ");"_ns}; auto insertStmt = DatabaseConnection::LazyStatement{ *aConnection, "INSERT OR IGNORE INTO index_data " "(index_id, value, object_data_key, " "object_store_id, value_locale) " "VALUES (:"_ns + kStmtParamNameIndexId + ", :"_ns + kStmtParamNameValue + ", :"_ns + kStmtParamNameObjectDataKey + ", :"_ns + kStmtParamNameObjectStoreId + ", :"_ns + kStmtParamNameValueLocale + ");"_ns}; for (uint32_t index = 0; index < count; index++) { const IndexDataValue& info = aIndexValues[index]; auto& stmt = info.mUnique ? insertUniqueStmt : insertStmt; QM_TRY_INSPECT(const auto& borrowedStmt, stmt.Borrow()); QM_TRY(MOZ_TO_RESULT( borrowedStmt->BindInt64ByName(kStmtParamNameIndexId, info.mIndexId))); QM_TRY(MOZ_TO_RESULT( info.mPosition.BindToStatement(&*borrowedStmt, kStmtParamNameValue))); QM_TRY(MOZ_TO_RESULT(info.mLocaleAwarePosition.BindToStatement( &*borrowedStmt, kStmtParamNameValueLocale))); QM_TRY(MOZ_TO_RESULT(borrowedStmt->BindInt64ByName( kStmtParamNameObjectStoreId, aObjectStoreId))); QM_TRY(MOZ_TO_RESULT(aObjectStoreKey.BindToStatement( &*borrowedStmt, kStmtParamNameObjectDataKey))); // QM_OR_ELSE_WARN_IF is not used here since we just want to log the // collision and not spam the reports. QM_TRY(QM_OR_ELSE_LOG_VERBOSE_IF( // Expression. MOZ_TO_RESULT(borrowedStmt->Execute()), // Predicate. ([&info, index, &aIndexValues](nsresult rv) { if (rv == NS_ERROR_STORAGE_CONSTRAINT && info.mUnique) { // If we're inserting multiple entries for the same unique // index, then we might have failed to insert due to // colliding with another entry for the same index in which // case we should ignore it. for (int32_t index2 = int32_t(index) - 1; index2 >= 0 && aIndexValues[index2].mIndexId == info.mIndexId; --index2) { if (info.mPosition == aIndexValues[index2].mPosition) { // We found a key with the same value for the same // index. So we must have had a collision with a value // we just inserted. return true; } } } return false; }), // Fallback. ErrToDefaultOk<>)); } return NS_OK; } // static nsresult DatabaseOperationBase::DeleteIndexDataTableRows( DatabaseConnection* aConnection, const Key& aObjectStoreKey, const nsTArray& aIndexValues) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(!aObjectStoreKey.IsUnset()); AUTO_PROFILER_LABEL("DatabaseOperationBase::DeleteIndexDataTableRows", DOM); const uint32_t count = aIndexValues.Length(); if (!count) { return NS_OK; } auto deleteUniqueStmt = DatabaseConnection::LazyStatement{ *aConnection, "DELETE FROM unique_index_data WHERE index_id = :"_ns + kStmtParamNameIndexId + " AND value = :"_ns + kStmtParamNameValue + ";"_ns}; auto deleteStmt = DatabaseConnection::LazyStatement{ *aConnection, "DELETE FROM index_data WHERE index_id = :"_ns + kStmtParamNameIndexId + " AND value = :"_ns + kStmtParamNameValue + " AND object_data_key = :"_ns + kStmtParamNameObjectDataKey + ";"_ns}; for (uint32_t index = 0; index < count; index++) { const IndexDataValue& indexValue = aIndexValues[index]; auto& stmt = indexValue.mUnique ? deleteUniqueStmt : deleteStmt; QM_TRY_INSPECT(const auto& borrowedStmt, stmt.Borrow()); QM_TRY(MOZ_TO_RESULT(borrowedStmt->BindInt64ByName(kStmtParamNameIndexId, indexValue.mIndexId))); QM_TRY(MOZ_TO_RESULT(indexValue.mPosition.BindToStatement( &*borrowedStmt, kStmtParamNameValue))); if (!indexValue.mUnique) { QM_TRY(MOZ_TO_RESULT(aObjectStoreKey.BindToStatement( &*borrowedStmt, kStmtParamNameObjectDataKey))); } QM_TRY(MOZ_TO_RESULT(borrowedStmt->Execute())); } return NS_OK; } // static nsresult DatabaseOperationBase::DeleteObjectStoreDataTableRowsWithIndexes( DatabaseConnection* aConnection, const IndexOrObjectStoreId aObjectStoreId, const Maybe& aKeyRange) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(aObjectStoreId); #ifdef DEBUG { QM_TRY_INSPECT(const bool& hasIndexes, ObjectStoreHasIndexes(*aConnection, aObjectStoreId), QM_PROPAGATE, [](const auto&) { MOZ_ASSERT(false); }); MOZ_ASSERT(hasIndexes, "Don't use this slow method if there are no indexes!"); } #endif AUTO_PROFILER_LABEL( "DatabaseOperationBase::DeleteObjectStoreDataTableRowsWithIndexes", DOM); const bool singleRowOnly = aKeyRange.isSome() && aKeyRange.ref().isOnly(); const auto keyRangeClause = MaybeGetBindingClauseForKeyRange(aKeyRange, kColumnNameKey); Key objectStoreKey; QM_TRY_INSPECT( const auto& selectStmt, ([singleRowOnly, &aConnection, &objectStoreKey, &aKeyRange, &keyRangeClause]() -> Result { if (singleRowOnly) { QM_TRY_UNWRAP(auto selectStmt, aConnection->BorrowCachedStatement( "SELECT index_data_values " "FROM object_data " "WHERE object_store_id = :"_ns + kStmtParamNameObjectStoreId + " AND key = :"_ns + kStmtParamNameKey + ";"_ns)); objectStoreKey = aKeyRange.ref().lower(); QM_TRY(MOZ_TO_RESULT( objectStoreKey.BindToStatement(&*selectStmt, kStmtParamNameKey))); return selectStmt; } QM_TRY_UNWRAP( auto selectStmt, aConnection->BorrowCachedStatement( "SELECT index_data_values, "_ns + kColumnNameKey + " FROM object_data WHERE object_store_id = :"_ns + kStmtParamNameObjectStoreId + keyRangeClause + ";"_ns)); if (aKeyRange.isSome()) { QM_TRY(MOZ_TO_RESULT( BindKeyRangeToStatement(aKeyRange.ref(), &*selectStmt))); } return selectStmt; }())); QM_TRY(MOZ_TO_RESULT(selectStmt->BindInt64ByName(kStmtParamNameObjectStoreId, aObjectStoreId))); DebugOnly resultCountDEBUG = 0; QM_TRY(CollectWhileHasResult( *selectStmt, [singleRowOnly, &objectStoreKey, &aConnection, &resultCountDEBUG, indexValues = IndexDataValuesAutoArray{}]( auto& selectStmt) mutable -> Result { if (!singleRowOnly) { QM_TRY( MOZ_TO_RESULT(objectStoreKey.SetFromStatement(&selectStmt, 1))); indexValues.ClearAndRetainStorage(); } QM_TRY(MOZ_TO_RESULT( ReadCompressedIndexDataValues(selectStmt, 0, indexValues))); QM_TRY(MOZ_TO_RESULT(DeleteIndexDataTableRows( aConnection, objectStoreKey, indexValues))); resultCountDEBUG++; return Ok{}; })); MOZ_ASSERT_IF(singleRowOnly, resultCountDEBUG <= 1); QM_TRY_UNWRAP( auto deleteManyStmt, aConnection->BorrowCachedStatement( "DELETE FROM object_data "_ns + "WHERE object_store_id = :"_ns + kStmtParamNameObjectStoreId + keyRangeClause + ";"_ns)); QM_TRY(MOZ_TO_RESULT(deleteManyStmt->BindInt64ByName( kStmtParamNameObjectStoreId, aObjectStoreId))); if (aKeyRange.isSome()) { QM_TRY(MOZ_TO_RESULT( BindKeyRangeToStatement(aKeyRange.ref(), &*deleteManyStmt))); } QM_TRY(MOZ_TO_RESULT(deleteManyStmt->Execute())); return NS_OK; } // static nsresult DatabaseOperationBase::UpdateIndexValues( DatabaseConnection* aConnection, const IndexOrObjectStoreId aObjectStoreId, const Key& aObjectStoreKey, const nsTArray& aIndexValues) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(!aObjectStoreKey.IsUnset()); AUTO_PROFILER_LABEL("DatabaseOperationBase::UpdateIndexValues", DOM); QM_TRY_UNWRAP((auto [indexDataValues, indexDataValuesLength]), MakeCompressedIndexDataValues(aIndexValues)); MOZ_ASSERT(!indexDataValuesLength == !(indexDataValues.get())); QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement( "UPDATE object_data SET index_data_values = :"_ns + kStmtParamNameIndexDataValues + " WHERE object_store_id = :"_ns + kStmtParamNameObjectStoreId + " AND key = :"_ns + kStmtParamNameKey + ";"_ns, [&indexDataValues = indexDataValues, indexDataValuesLength = indexDataValuesLength, aObjectStoreId, &aObjectStoreKey]( mozIStorageStatement& updateStmt) -> Result { QM_TRY(MOZ_TO_RESULT( indexDataValues ? updateStmt.BindAdoptedBlobByName( kStmtParamNameIndexDataValues, indexDataValues.release(), indexDataValuesLength) : updateStmt.BindNullByName(kStmtParamNameIndexDataValues))); QM_TRY(MOZ_TO_RESULT(updateStmt.BindInt64ByName( kStmtParamNameObjectStoreId, aObjectStoreId))); QM_TRY(MOZ_TO_RESULT( aObjectStoreKey.BindToStatement(&updateStmt, kStmtParamNameKey))); return Ok{}; }))); return NS_OK; } // static Result DatabaseOperationBase::ObjectStoreHasIndexes( DatabaseConnection& aConnection, const IndexOrObjectStoreId aObjectStoreId) { aConnection.AssertIsOnConnectionThread(); MOZ_ASSERT(aObjectStoreId); QM_TRY_RETURN(aConnection .BorrowAndExecuteSingleStepStatement( "SELECT id " "FROM object_store_index " "WHERE object_store_id = :"_ns + kStmtParamNameObjectStoreId + kOpenLimit + "1;"_ns, [aObjectStoreId](auto& stmt) -> Result { QM_TRY(MOZ_TO_RESULT(stmt.BindInt64ByName( kStmtParamNameObjectStoreId, aObjectStoreId))); return Ok{}; }) .map(IsSome)); } NS_IMPL_ISUPPORTS_INHERITED(DatabaseOperationBase, Runnable, mozIStorageProgressHandler) NS_IMETHODIMP DatabaseOperationBase::OnProgress(mozIStorageConnection* aConnection, bool* _retval) { MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(_retval); // This is intentionally racy. *_retval = QuotaClient::IsShuttingDownOnNonBackgroundThread() || !OperationMayProceed(); return NS_OK; } DatabaseOperationBase::AutoSetProgressHandler::AutoSetProgressHandler() : mConnection(Nothing()) #ifdef DEBUG , mDEBUGDatabaseOp(nullptr) #endif { MOZ_ASSERT(!IsOnBackgroundThread()); } DatabaseOperationBase::AutoSetProgressHandler::~AutoSetProgressHandler() { MOZ_ASSERT(!IsOnBackgroundThread()); if (mConnection) { Unregister(); } } nsresult DatabaseOperationBase::AutoSetProgressHandler::Register( mozIStorageConnection& aConnection, DatabaseOperationBase* aDatabaseOp) { MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(aDatabaseOp); MOZ_ASSERT(!mConnection); QM_TRY_UNWRAP( const DebugOnly oldProgressHandler, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( nsCOMPtr, aConnection, SetProgressHandler, kStorageProgressGranularity, aDatabaseOp)); MOZ_ASSERT(!oldProgressHandler.inspect()); mConnection = SomeRef(aConnection); #ifdef DEBUG mDEBUGDatabaseOp = aDatabaseOp; #endif return NS_OK; } void DatabaseOperationBase::AutoSetProgressHandler::Unregister() { MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(mConnection); nsCOMPtr oldHandler; MOZ_ALWAYS_SUCCEEDS( mConnection->RemoveProgressHandler(getter_AddRefs(oldHandler))); MOZ_ASSERT(oldHandler == mDEBUGDatabaseOp); mConnection = Nothing(); } FactoryOp::FactoryOp(SafeRefPtr aFactory, const Maybe& aContentParentId, const CommonFactoryRequestParams& aCommonParams, bool aDeleting) : DatabaseOperationBase(aFactory->GetLoggingInfo()->Id(), aFactory->GetLoggingInfo()->NextRequestSN()), mFactory(std::move(aFactory)), mContentParentId(aContentParentId), mCommonParams(aCommonParams), mDirectoryLockId(-1), mState(State::Initial), mWaitingForPermissionRetry(false), mEnforcingQuota(true), mDeleting(aDeleting) { AssertIsOnBackgroundThread(); MOZ_ASSERT(mFactory); MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); } void FactoryOp::NoteDatabaseBlocked(Database* aDatabase) { AssertIsOnOwningThread(); MOZ_ASSERT(aDatabase); MOZ_ASSERT(mState == State::WaitingForOtherDatabasesToClose); MOZ_ASSERT(!mMaybeBlockedDatabases.IsEmpty()); MOZ_ASSERT(mMaybeBlockedDatabases.Contains(aDatabase)); // Only send the blocked event if all databases have reported back. If the // database was closed then it will have been removed from the array. // Otherwise if it was blocked its |mBlocked| flag will be true. bool sendBlockedEvent = true; for (auto& info : mMaybeBlockedDatabases) { if (info == aDatabase) { // This database was blocked, mark accordingly. info.mBlocked = true; } else if (!info.mBlocked) { // A database has not yet reported back yet, don't send the event yet. sendBlockedEvent = false; } } if (sendBlockedEvent) { SendBlockedNotification(); } } void FactoryOp::NoteDatabaseClosed(Database* const aDatabase) { AssertIsOnOwningThread(); MOZ_ASSERT(aDatabase); MOZ_ASSERT(mState == State::WaitingForOtherDatabasesToClose); MOZ_ASSERT(!mMaybeBlockedDatabases.IsEmpty()); MOZ_ASSERT(mMaybeBlockedDatabases.Contains(aDatabase)); mMaybeBlockedDatabases.RemoveElement(aDatabase); if (!mMaybeBlockedDatabases.IsEmpty()) { return; } DatabaseActorInfo* info; MOZ_ALWAYS_TRUE(gLiveDatabaseHashtable->Get(mDatabaseId, &info)); MOZ_ASSERT(info->mWaitingFactoryOp == this); if (AreActorsAlive()) { // The IPDL strong reference has not yet been released, so we can clear // mWaitingFactoryOp immediately. info->mWaitingFactoryOp = nullptr; WaitForTransactions(); return; } // The IPDL strong reference has been released, mWaitingFactoryOp holds the // last strong reference to us, so we need to move it to a stack variable // instead of clearing it immediately (We could clear it immediately if only // the other actor is destroyed, but we don't need to optimize for that, and // move it anyway). const RefPtr waitingFactoryOp = std::move(info->mWaitingFactoryOp); IDB_REPORT_INTERNAL_ERR(); SetFailureCodeIfUnset(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); // We hold a strong ref in waitingFactoryOp, so it's safe to call Run() // directly. mState = State::SendingResults; MOZ_ALWAYS_SUCCEEDS(Run()); } void FactoryOp::StringifyState(nsACString& aResult) const { AssertIsOnOwningThread(); switch (mState) { case State::Initial: aResult.AppendLiteral("Initial"); return; case State::FinishOpen: aResult.AppendLiteral("FinishOpen"); return; case State::DirectoryOpenPending: aResult.AppendLiteral("DirectoryOpenPending"); return; case State::DatabaseOpenPending: aResult.AppendLiteral("DatabaseOpenPending"); return; case State::DatabaseWorkOpen: aResult.AppendLiteral("DatabaseWorkOpen"); return; case State::BeginVersionChange: aResult.AppendLiteral("BeginVersionChange"); return; case State::WaitingForOtherDatabasesToClose: aResult.AppendLiteral("WaitingForOtherDatabasesToClose"); return; case State::WaitingForTransactionsToComplete: aResult.AppendLiteral("WaitingForTransactionsToComplete"); return; case State::DatabaseWorkVersionChange: aResult.AppendLiteral("DatabaseWorkVersionChange"); return; case State::SendingResults: aResult.AppendLiteral("SendingResults"); return; case State::Completed: aResult.AppendLiteral("Completed"); return; default: MOZ_CRASH("Bad state!"); } } void FactoryOp::Stringify(nsACString& aResult) const { AssertIsOnOwningThread(); aResult.AppendLiteral("PersistenceType:"); aResult.Append( PersistenceTypeToString(mCommonParams.metadata().persistenceType())); aResult.Append(kQuotaGenericDelimiter); aResult.AppendLiteral("Origin:"); aResult.Append(AnonymizedOriginString(mOriginMetadata.mOrigin)); aResult.Append(kQuotaGenericDelimiter); aResult.AppendLiteral("State:"); StringifyState(aResult); } nsresult FactoryOp::Open() { AssertIsOnMainThread(); MOZ_ASSERT(mState == State::Initial); if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || !OperationMayProceed()) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } const PrincipalInfo& principalInfo = mCommonParams.principalInfo(); if (principalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) { MOZ_ASSERT(mCommonParams.metadata().persistenceType() == PERSISTENCE_TYPE_PERSISTENT); } else if (principalInfo.type() == PrincipalInfo::TContentPrincipalInfo) { const ContentPrincipalInfo& contentPrincipalInfo = principalInfo.get_ContentPrincipalInfo(); if (contentPrincipalInfo.attrs().mPrivateBrowsingId != 0) { if (StaticPrefs::dom_indexedDB_privateBrowsing_enabled()) { // Explicitly disallow moz-extension urls from using the encrypted // indexedDB storage mode when the caller is an extension (see Bug // 1841806). if (StringBeginsWith(contentPrincipalInfo.originNoSuffix(), "moz-extension:"_ns)) { return NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR; } mInPrivateBrowsing.Flip(); } else { return NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR; } } } else { MOZ_ASSERT(false); } QM_TRY_INSPECT(const auto& permission, CheckPermission()); MOZ_ASSERT(permission == PermissionValue::kPermissionAllowed || permission == PermissionValue::kPermissionDenied); if (permission == PermissionValue::kPermissionDenied) { return NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR; } { // These services have to be started on the main thread currently. IndexedDatabaseManager* mgr; if (NS_WARN_IF(!(mgr = IndexedDatabaseManager::GetOrCreate()))) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } nsCOMPtr ss; if (NS_WARN_IF(!(ss = do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID)))) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } } MOZ_ASSERT(permission == PermissionValue::kPermissionAllowed); mState = State::FinishOpen; MOZ_ALWAYS_SUCCEEDS(mOwningEventTarget->Dispatch(this, NS_DISPATCH_NORMAL)); return NS_OK; } nsresult FactoryOp::DirectoryOpen() { AssertIsOnOwningThread(); MOZ_ASSERT(mState == State::DirectoryOpenPending); MOZ_ASSERT(mDirectoryLock); MOZ_ASSERT(!mDatabaseFilePath.IsEmpty()); MOZ_ASSERT(gFactoryOps); // See if this FactoryOp needs to wait. const bool delayed = std::any_of( gFactoryOps->rbegin(), gFactoryOps->rend(), [foundThis = false, &self = *this](const auto& existingOp) mutable { if (existingOp == &self) { foundThis = true; return false; } if (foundThis && self.MustWaitFor(*existingOp)) { // Only one op can be delayed. MOZ_ASSERT(!existingOp->mDelayedOp); existingOp->mDelayedOp = &self; return true; } return false; }) || [&self = *this] { QuotaClient* quotaClient = QuotaClient::GetInstance(); MOZ_ASSERT(quotaClient); if (RefPtr currentMaintenance = quotaClient->GetCurrentMaintenance()) { if (RefPtr databaseMaintenance = currentMaintenance->GetDatabaseMaintenance( self.mDatabaseFilePath)) { databaseMaintenance->WaitForCompletion(&self); return true; } } return false; }(); mState = State::DatabaseOpenPending; if (!delayed) { QM_TRY(MOZ_TO_RESULT(DatabaseOpen())); } return NS_OK; } nsresult FactoryOp::SendToIOThread() { AssertIsOnOwningThread(); MOZ_ASSERT(mState == State::DatabaseOpenPending); if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || !OperationMayProceed()) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } QuotaManager* const quotaManager = QuotaManager::Get(); MOZ_ASSERT(quotaManager); // Must set this before dispatching otherwise we will race with the IO thread. mState = State::DatabaseWorkOpen; QM_TRY(MOZ_TO_RESULT( quotaManager->IOThread()->Dispatch(this, NS_DISPATCH_NORMAL)), NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR, IDB_REPORT_INTERNAL_ERR_LAMBDA); return NS_OK; } void FactoryOp::WaitForTransactions() { AssertIsOnOwningThread(); MOZ_ASSERT(mState == State::BeginVersionChange || mState == State::WaitingForOtherDatabasesToClose); MOZ_ASSERT(!mDatabaseId.IsEmpty()); MOZ_ASSERT(!IsActorDestroyed()); mState = State::WaitingForTransactionsToComplete; RefPtr helper = new WaitForTransactionsHelper(mDatabaseId, this); helper->WaitForTransactions(); } void FactoryOp::CleanupMetadata() { AssertIsOnOwningThread(); if (mDelayedOp) { MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(mDelayedOp.forget())); } MOZ_ASSERT(gFactoryOps); gFactoryOps->RemoveElement(this); // We might get here even after QuotaManagerOpen failed, so we need to check // if we have a quota manager. quota::QuotaManager::SafeMaybeRecordQuotaClientShutdownStep( quota::Client::IDB, "An element was removed from gFactoryOps"_ns); // Match the IncreaseBusyCount in AllocPBackgroundIDBFactoryRequestParent(). DecreaseBusyCount(); } void FactoryOp::FinishSendResults() { AssertIsOnOwningThread(); MOZ_ASSERT(mState == State::SendingResults); MOZ_ASSERT(mFactory); mState = State::Completed; // Make sure to release the factory on this thread. mFactory = nullptr; } Result FactoryOp::CheckPermission() { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mState == State::Initial); const PrincipalInfo& principalInfo = mCommonParams.principalInfo(); MOZ_ASSERT(principalInfo.type() == PrincipalInfo::TSystemPrincipalInfo || principalInfo.type() == PrincipalInfo::TContentPrincipalInfo); if (principalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) { MOZ_ASSERT(mState == State::Initial); return PermissionValue::kPermissionAllowed; } QM_TRY_INSPECT( const auto& permission, ([persistenceType = mCommonParams.metadata().persistenceType(), origin = QuotaManager::GetOriginFromValidatedPrincipalInfo( principalInfo)]() -> mozilla::Result { if (persistenceType == PERSISTENCE_TYPE_PERSISTENT) { if (QuotaManager::IsOriginInternal(origin)) { return PermissionValue::kPermissionAllowed; } return Err(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); } return PermissionValue::kPermissionAllowed; })()); return permission; } nsresult FactoryOp::SendVersionChangeMessages( DatabaseActorInfo* aDatabaseActorInfo, Maybe aOpeningDatabase, uint64_t aOldVersion, const Maybe& aNewVersion) { AssertIsOnOwningThread(); MOZ_ASSERT(aDatabaseActorInfo); MOZ_ASSERT(mState == State::BeginVersionChange); MOZ_ASSERT(mMaybeBlockedDatabases.IsEmpty()); MOZ_ASSERT(!IsActorDestroyed()); const uint32_t expectedCount = mDeleting ? 0 : 1; const uint32_t liveCount = aDatabaseActorInfo->mLiveDatabases.Length(); if (liveCount > expectedCount) { nsTArray maybeBlockedDatabases; for (const auto& database : aDatabaseActorInfo->mLiveDatabases) { if ((!aOpeningDatabase || database.get() != &aOpeningDatabase.ref()) && !database->IsClosed() && NS_WARN_IF(!maybeBlockedDatabases.AppendElement( SafeRefPtr{database.get(), AcquireStrongRefFromRawPtr{}}, fallible))) { return NS_ERROR_OUT_OF_MEMORY; } } mMaybeBlockedDatabases = std::move(maybeBlockedDatabases); } // We don't want to wait forever if we were not able to send the // message. mMaybeBlockedDatabases.RemoveLastElements( mMaybeBlockedDatabases.end() - std::remove_if(mMaybeBlockedDatabases.begin(), mMaybeBlockedDatabases.end(), [aOldVersion, &aNewVersion](auto& maybeBlockedDatabase) { return !maybeBlockedDatabase->SendVersionChange( aOldVersion, aNewVersion); })); return NS_OK; } // namespace indexedDB nsresult FactoryOp::FinishOpen() { AssertIsOnOwningThread(); MOZ_ASSERT(mState == State::FinishOpen); MOZ_ASSERT(mOriginMetadata.mOrigin.IsEmpty()); MOZ_ASSERT(!mDirectoryLock); if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || IsActorDestroyed()) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } QM_TRY(QuotaManager::EnsureCreated()); QuotaManager* const quotaManager = QuotaManager::Get(); MOZ_ASSERT(quotaManager); const PrincipalInfo& principalInfo = mCommonParams.principalInfo(); const DatabaseMetadata& metadata = mCommonParams.metadata(); const PersistenceType persistenceType = metadata.persistenceType(); if (principalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) { mOriginMetadata = {QuotaManager::GetInfoForChrome(), persistenceType}; MOZ_ASSERT(QuotaManager::IsOriginInternal(mOriginMetadata.mOrigin)); mEnforcingQuota = false; } else { MOZ_ASSERT(principalInfo.type() == PrincipalInfo::TContentPrincipalInfo); QM_TRY_UNWRAP( auto principalMetadata, quotaManager->GetInfoFromValidatedPrincipalInfo(principalInfo)); mOriginMetadata = {std::move(principalMetadata), persistenceType}; mEnforcingQuota = persistenceType != PERSISTENCE_TYPE_PERSISTENT; } QuotaManager::GetStorageId(persistenceType, mOriginMetadata.mOrigin, Client::IDB, mDatabaseId); mDatabaseId.Append('*'); mDatabaseId.Append(NS_ConvertUTF16toUTF8(metadata.name())); // Need to get database file path before opening the directory. // XXX: For what reason? QM_TRY_UNWRAP( mDatabaseFilePath, ([this, metadata, quotaManager]() -> mozilla::Result { QM_TRY_INSPECT(const auto& dbFile, quotaManager->GetOriginDirectory(mOriginMetadata)); QM_TRY(MOZ_TO_RESULT(dbFile->Append( NS_LITERAL_STRING_FROM_CSTRING(IDB_DIRECTORY_NAME)))); QM_TRY(MOZ_TO_RESULT( dbFile->Append(GetDatabaseFilenameBase(metadata.name(), mOriginMetadata.mIsPrivate) + kSQLiteSuffix))); QM_TRY_RETURN( MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, dbFile, GetPath)); }())); // Open directory mState = State::DirectoryOpenPending; quotaManager->OpenClientDirectory({mOriginMetadata, Client::IDB}) ->Then( GetCurrentSerialEventTarget(), __func__, [self = RefPtr(this)]( const ClientDirectoryLockPromise::ResolveOrRejectValue& aValue) { if (aValue.IsResolve()) { self->DirectoryLockAcquired(aValue.ResolveValue()); } else { self->DirectoryLockFailed(); } }); return NS_OK; } bool FactoryOp::MustWaitFor(const FactoryOp& aExistingOp) { AssertIsOnOwningThread(); // Things for the same persistence type, the same origin and the same // database must wait. return aExistingOp.mCommonParams.metadata().persistenceType() == mCommonParams.metadata().persistenceType() && aExistingOp.mOriginMetadata.mOrigin == mOriginMetadata.mOrigin && aExistingOp.mDatabaseId == mDatabaseId; } // Run() assumes that the caller holds a strong reference to the object that // can't be cleared while Run() is being executed. // So if you call Run() directly (as opposed to dispatching to an event queue) // you need to make sure there's such a reference. // See bug 1356824 for more details. NS_IMETHODIMP FactoryOp::Run() { const auto handleError = [this](const nsresult rv) { if (mState != State::SendingResults) { SetFailureCodeIfUnset(rv); // Must set mState before dispatching otherwise we will race with the // owning thread. mState = State::SendingResults; if (IsOnOwningThread()) { SendResults(); } else { MOZ_ALWAYS_SUCCEEDS( mOwningEventTarget->Dispatch(this, NS_DISPATCH_NORMAL)); } } }; switch (mState) { case State::Initial: QM_WARNONLY_TRY(MOZ_TO_RESULT(Open()), handleError); break; case State::FinishOpen: QM_WARNONLY_TRY(MOZ_TO_RESULT(FinishOpen()), handleError); break; case State::DatabaseOpenPending: QM_WARNONLY_TRY(MOZ_TO_RESULT(DatabaseOpen()), handleError); break; case State::DatabaseWorkOpen: QM_WARNONLY_TRY(MOZ_TO_RESULT(DoDatabaseWork()), handleError); break; case State::BeginVersionChange: QM_WARNONLY_TRY(MOZ_TO_RESULT(BeginVersionChange()), handleError); break; case State::WaitingForTransactionsToComplete: QM_WARNONLY_TRY(MOZ_TO_RESULT(DispatchToWorkThread()), handleError); break; case State::SendingResults: SendResults(); break; default: MOZ_CRASH("Bad state!"); } return NS_OK; } void FactoryOp::DirectoryLockAcquired(DirectoryLock* aLock) { AssertIsOnOwningThread(); MOZ_ASSERT(aLock); MOZ_ASSERT(mState == State::DirectoryOpenPending); MOZ_ASSERT(!mDirectoryLock); mDirectoryLock = aLock; MOZ_ASSERT(mDirectoryLock->Id() >= 0); mDirectoryLockId = mDirectoryLock->Id(); QM_WARNONLY_TRY(MOZ_TO_RESULT(DirectoryOpen()), [this](const nsresult rv) { SetFailureCodeIfUnset(rv); // The caller holds a strong reference to us, no need for a self reference // before calling Run(). mState = State::SendingResults; MOZ_ALWAYS_SUCCEEDS(Run()); }); } void FactoryOp::DirectoryLockFailed() { AssertIsOnOwningThread(); MOZ_ASSERT(mState == State::DirectoryOpenPending); MOZ_ASSERT(!mDirectoryLock); if (!HasFailed()) { IDB_REPORT_INTERNAL_ERR(); SetFailureCode(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); } // The caller holds a strong reference to us, no need for a self reference // before calling Run(). mState = State::SendingResults; MOZ_ALWAYS_SUCCEEDS(Run()); } void FactoryOp::ActorDestroy(ActorDestroyReason aWhy) { AssertIsOnBackgroundThread(); NoteActorDestroyed(); } OpenDatabaseOp::OpenDatabaseOp(SafeRefPtr aFactory, const Maybe& aContentParentId, const CommonFactoryRequestParams& aParams) : FactoryOp(std::move(aFactory), aContentParentId, aParams, /* aDeleting */ false), mMetadata(MakeSafeRefPtr(aParams.metadata())), mRequestedVersion(aParams.metadata().version()), mVersionChangeOp(nullptr), mTelemetryId(0) {} void OpenDatabaseOp::ActorDestroy(ActorDestroyReason aWhy) { AssertIsOnOwningThread(); FactoryOp::ActorDestroy(aWhy); if (mVersionChangeOp) { mVersionChangeOp->NoteActorDestroyed(); } } nsresult OpenDatabaseOp::DatabaseOpen() { AssertIsOnOwningThread(); MOZ_ASSERT(mState == State::DatabaseOpenPending); nsresult rv = SendToIOThread(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } nsresult OpenDatabaseOp::DoDatabaseWork() { AssertIsOnIOThread(); MOZ_ASSERT(mState == State::DatabaseWorkOpen); AUTO_PROFILER_LABEL("OpenDatabaseOp::DoDatabaseWork", DOM); QM_TRY(OkIf(!QuotaClient::IsShuttingDownOnNonBackgroundThread()), NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR, IDB_REPORT_INTERNAL_ERR_LAMBDA); if (!OperationMayProceed()) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } const nsAString& databaseName = mCommonParams.metadata().name(); const PersistenceType persistenceType = mCommonParams.metadata().persistenceType(); QuotaManager* const quotaManager = QuotaManager::Get(); MOZ_ASSERT(quotaManager); QM_TRY_INSPECT( const auto& dbDirectory, ([persistenceType, "aManager, this]() -> mozilla::Result, bool>, nsresult> { if (persistenceType == PERSISTENCE_TYPE_PERSISTENT) { QM_TRY_RETURN(quotaManager->EnsurePersistentOriginIsInitialized( mOriginMetadata)); } QM_TRY(MOZ_TO_RESULT( quotaManager->EnsureTemporaryStorageIsInitializedInternal())); QM_TRY_RETURN(quotaManager->EnsureTemporaryOriginIsInitialized( persistenceType, mOriginMetadata)); }() .map([](const auto& res) { return res.first; }))); QM_TRY(MOZ_TO_RESULT( dbDirectory->Append(NS_LITERAL_STRING_FROM_CSTRING(IDB_DIRECTORY_NAME)))); { QM_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE_MEMBER(dbDirectory, Exists)); if (!exists) { QM_TRY(MOZ_TO_RESULT(dbDirectory->Create(nsIFile::DIRECTORY_TYPE, 0755))); } #ifdef DEBUG else { bool isDirectory; MOZ_ASSERT(NS_SUCCEEDED(dbDirectory->IsDirectory(&isDirectory))); MOZ_ASSERT(isDirectory); } #endif } const auto databaseFilenameBase = GetDatabaseFilenameBase(databaseName, mOriginMetadata.mIsPrivate); QM_TRY_INSPECT(const auto& markerFile, CloneFileAndAppend(*dbDirectory, kIdbDeletionMarkerFilePrefix + databaseFilenameBase)); QM_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE_MEMBER(markerFile, Exists)); if (exists) { // Delete the database and directroy since they should be deleted in // previous operation. // Note: only update usage to the QuotaManager when mEnforcingQuota == true QM_TRY(MOZ_TO_RESULT(RemoveDatabaseFilesAndDirectory( *dbDirectory, databaseFilenameBase, mEnforcingQuota ? quotaManager : nullptr, persistenceType, mOriginMetadata, databaseName))); } QM_TRY_INSPECT( const auto& dbFile, CloneFileAndAppend(*dbDirectory, databaseFilenameBase + kSQLiteSuffix)); mTelemetryId = TelemetryIdForFile(dbFile); #ifdef DEBUG { QM_TRY_INSPECT( const auto& databaseFilePath, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, dbFile, GetPath)); MOZ_ASSERT(databaseFilePath == mDatabaseFilePath); } #endif QM_TRY_INSPECT( const auto& fmDirectory, CloneFileAndAppend(*dbDirectory, databaseFilenameBase + kFileManagerDirectoryNameSuffix)); IndexedDatabaseManager* const idm = IndexedDatabaseManager::Get(); MOZ_ASSERT(idm); SafeRefPtr fileManager = idm->GetFileManager( persistenceType, mOriginMetadata.mOrigin, databaseName); if (!fileManager) { fileManager = MakeSafeRefPtr( persistenceType, mOriginMetadata, databaseName, mDatabaseId, mEnforcingQuota, mInPrivateBrowsing); } Maybe maybeKey = mInPrivateBrowsing ? Some(fileManager->MutableCipherKeyManagerRef().Ensure()) : Nothing(); MOZ_RELEASE_ASSERT(mInPrivateBrowsing == maybeKey.isSome()); QM_TRY_UNWRAP( NotNull> connection, CreateStorageConnection(*dbFile, *fmDirectory, databaseName, mOriginMetadata.mOrigin, mDirectoryLockId, mTelemetryId, maybeKey)); AutoSetProgressHandler asph; QM_TRY(MOZ_TO_RESULT(asph.Register(*connection, this))); QM_TRY(MOZ_TO_RESULT(LoadDatabaseInformation(*connection))); MOZ_ASSERT(mMetadata->mNextObjectStoreId > mMetadata->mObjectStores.Count()); MOZ_ASSERT(mMetadata->mNextIndexId > 0); // See if we need to do a versionchange transaction // Optional version semantics. if (!mRequestedVersion) { // If the requested version was not specified and the database was created, // treat it as if version 1 were requested. // Otherwise, treat it as if the current version were requested. mRequestedVersion = mMetadata->mCommonMetadata.version() == 0 ? 1 : mMetadata->mCommonMetadata.version(); } QM_TRY(OkIf(mMetadata->mCommonMetadata.version() <= mRequestedVersion), NS_ERROR_DOM_INDEXEDDB_VERSION_ERR); if (!fileManager->Initialized()) { QM_TRY(MOZ_TO_RESULT(fileManager->Init(fmDirectory, *connection))); idm->AddFileManager(fileManager.clonePtr()); } mFileManager = std::move(fileManager); // Must close connection before dispatching otherwise we might race with the // connection thread which needs to open the same database. asph.Unregister(); MOZ_ALWAYS_SUCCEEDS(connection->Close()); // Must set mState before dispatching otherwise we will race with the owning // thread. mState = (mMetadata->mCommonMetadata.version() == mRequestedVersion) ? State::SendingResults : State::BeginVersionChange; QM_TRY(MOZ_TO_RESULT(mOwningEventTarget->Dispatch(this, NS_DISPATCH_NORMAL))); return NS_OK; } nsresult OpenDatabaseOp::LoadDatabaseInformation( mozIStorageConnection& aConnection) { AssertIsOnIOThread(); MOZ_ASSERT(mMetadata); { // Load version information. QM_TRY_INSPECT( const auto& stmt, CreateAndExecuteSingleStepStatement< SingleStepResult::ReturnNullIfNoResult>( aConnection, "SELECT name, origin, version FROM database"_ns)); QM_TRY(OkIf(stmt), NS_ERROR_FILE_CORRUPTED); QM_TRY_INSPECT(const auto& databaseName, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( nsString, stmt, GetString, 0)); QM_TRY(OkIf(mCommonParams.metadata().name() == databaseName), NS_ERROR_FILE_CORRUPTED); QM_TRY_INSPECT(const auto& origin, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( nsCString, stmt, GetUTF8String, 1)); // We can't just compare these strings directly. See bug 1339081 comment 69. QM_TRY(OkIf(QuotaManager::AreOriginsEqualOnDisk(mOriginMetadata.mOrigin, origin)), NS_ERROR_FILE_CORRUPTED); QM_TRY_INSPECT(const int64_t& version, MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt64, 2)); mMetadata->mCommonMetadata.version() = uint64_t(version); } ObjectStoreTable& objectStores = mMetadata->mObjectStores; QM_TRY_INSPECT( const auto& lastObjectStoreId, ([&aConnection, &objectStores]() -> mozilla::Result { // Load object store names and ids. QM_TRY_INSPECT( const auto& stmt, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( nsCOMPtr, aConnection, CreateStatement, "SELECT id, auto_increment, name, key_path " "FROM object_store"_ns)); IndexOrObjectStoreId lastObjectStoreId = 0; QM_TRY(CollectWhileHasResult( *stmt, [&lastObjectStoreId, &objectStores, usedIds = Maybe>{}, usedNames = Maybe>{}]( auto& stmt) mutable -> mozilla::Result { QM_TRY_INSPECT(const IndexOrObjectStoreId& objectStoreId, MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt64, 0)); if (!usedIds) { usedIds.emplace(); } QM_TRY(OkIf(objectStoreId > 0), Err(NS_ERROR_FILE_CORRUPTED)); QM_TRY(OkIf(!usedIds.ref().Contains(objectStoreId)), Err(NS_ERROR_FILE_CORRUPTED)); QM_TRY(OkIf(usedIds.ref().Insert(objectStoreId, fallible)), Err(NS_ERROR_OUT_OF_MEMORY)); nsString name; QM_TRY(MOZ_TO_RESULT(stmt.GetString(2, name))); if (!usedNames) { usedNames.emplace(); } QM_TRY(OkIf(!usedNames.ref().Contains(name)), Err(NS_ERROR_FILE_CORRUPTED)); QM_TRY(OkIf(usedNames.ref().Insert(name, fallible)), Err(NS_ERROR_OUT_OF_MEMORY)); ObjectStoreMetadata commonMetadata; commonMetadata.id() = objectStoreId; commonMetadata.name() = std::move(name); QM_TRY_INSPECT( const int32_t& columnType, MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetTypeOfIndex, 3)); if (columnType == mozIStorageStatement::VALUE_TYPE_NULL) { commonMetadata.keyPath() = KeyPath(0); } else { MOZ_ASSERT(columnType == mozIStorageStatement::VALUE_TYPE_TEXT); nsString keyPathSerialization; QM_TRY(MOZ_TO_RESULT(stmt.GetString(3, keyPathSerialization))); commonMetadata.keyPath() = KeyPath::DeserializeFromString(keyPathSerialization); QM_TRY(OkIf(commonMetadata.keyPath().IsValid()), Err(NS_ERROR_FILE_CORRUPTED)); } QM_TRY_INSPECT(const int64_t& nextAutoIncrementId, MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt64, 1)); commonMetadata.autoIncrement() = !!nextAutoIncrementId; QM_TRY(OkIf(objectStores.InsertOrUpdate( objectStoreId, MakeSafeRefPtr( std::move(commonMetadata), FullObjectStoreMetadata::AutoIncrementIds{ nextAutoIncrementId, nextAutoIncrementId}), fallible)), Err(NS_ERROR_OUT_OF_MEMORY)); lastObjectStoreId = std::max(lastObjectStoreId, objectStoreId); return Ok{}; })); return lastObjectStoreId; }())); QM_TRY_INSPECT( const auto& lastIndexId, ([&aConnection, &objectStores]() -> mozilla::Result { // Load index information QM_TRY_INSPECT( const auto& stmt, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( nsCOMPtr, aConnection, CreateStatement, "SELECT " "id, object_store_id, name, key_path, " "unique_index, multientry, " "locale, is_auto_locale " "FROM object_store_index"_ns)); IndexOrObjectStoreId lastIndexId = 0; QM_TRY(CollectWhileHasResult( *stmt, [&lastIndexId, &objectStores, &aConnection, usedIds = Maybe>{}, usedNames = Maybe>{}]( auto& stmt) mutable -> mozilla::Result { QM_TRY_INSPECT(const IndexOrObjectStoreId& objectStoreId, MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt64, 1)); // XXX Why does this return NS_ERROR_OUT_OF_MEMORY if we don't // know the object store id? auto objectStoreMetadata = objectStores.Lookup(objectStoreId); QM_TRY(OkIf(static_cast(objectStoreMetadata)), Err(NS_ERROR_OUT_OF_MEMORY)); MOZ_ASSERT((*objectStoreMetadata)->mCommonMetadata.id() == objectStoreId); IndexOrObjectStoreId indexId; QM_TRY(MOZ_TO_RESULT(stmt.GetInt64(0, &indexId))); if (!usedIds) { usedIds.emplace(); } QM_TRY(OkIf(indexId > 0), Err(NS_ERROR_FILE_CORRUPTED)); QM_TRY(OkIf(!usedIds.ref().Contains(indexId)), Err(NS_ERROR_FILE_CORRUPTED)); QM_TRY(OkIf(usedIds.ref().Insert(indexId, fallible)), Err(NS_ERROR_OUT_OF_MEMORY)); nsString name; QM_TRY(MOZ_TO_RESULT(stmt.GetString(2, name))); const nsAutoString hashName = IntToString(indexId) + u":"_ns + name; if (!usedNames) { usedNames.emplace(); } QM_TRY(OkIf(!usedNames.ref().Contains(hashName)), Err(NS_ERROR_FILE_CORRUPTED)); QM_TRY(OkIf(usedNames.ref().Insert(hashName, fallible)), Err(NS_ERROR_OUT_OF_MEMORY)); auto indexMetadata = MakeSafeRefPtr(); indexMetadata->mCommonMetadata.id() = indexId; indexMetadata->mCommonMetadata.name() = name; #ifdef DEBUG { int32_t columnType; nsresult rv = stmt.GetTypeOfIndex(3, &columnType); MOZ_ASSERT(NS_SUCCEEDED(rv)); MOZ_ASSERT(columnType != mozIStorageStatement::VALUE_TYPE_NULL); } #endif nsString keyPathSerialization; QM_TRY(MOZ_TO_RESULT(stmt.GetString(3, keyPathSerialization))); indexMetadata->mCommonMetadata.keyPath() = KeyPath::DeserializeFromString(keyPathSerialization); QM_TRY(OkIf(indexMetadata->mCommonMetadata.keyPath().IsValid()), Err(NS_ERROR_FILE_CORRUPTED)); int32_t scratch; QM_TRY(MOZ_TO_RESULT(stmt.GetInt32(4, &scratch))); indexMetadata->mCommonMetadata.unique() = !!scratch; QM_TRY(MOZ_TO_RESULT(stmt.GetInt32(5, &scratch))); indexMetadata->mCommonMetadata.multiEntry() = !!scratch; const bool localeAware = !stmt.IsNull(6); if (localeAware) { QM_TRY(MOZ_TO_RESULT(stmt.GetUTF8String( 6, indexMetadata->mCommonMetadata.locale()))); QM_TRY(MOZ_TO_RESULT(stmt.GetInt32(7, &scratch))); indexMetadata->mCommonMetadata.autoLocale() = !!scratch; // Update locale-aware indexes if necessary const nsCString& indexedLocale = indexMetadata->mCommonMetadata.locale(); const bool& isAutoLocale = indexMetadata->mCommonMetadata.autoLocale(); const nsCString& systemLocale = IndexedDatabaseManager::GetLocale(); if (!systemLocale.IsEmpty() && isAutoLocale && !indexedLocale.Equals(systemLocale)) { QM_TRY(MOZ_TO_RESULT(UpdateLocaleAwareIndex( aConnection, indexMetadata->mCommonMetadata, systemLocale))); } } QM_TRY(OkIf((*objectStoreMetadata) ->mIndexes.InsertOrUpdate( indexId, std::move(indexMetadata), fallible)), Err(NS_ERROR_OUT_OF_MEMORY)); lastIndexId = std::max(lastIndexId, indexId); return Ok{}; })); return lastIndexId; }())); QM_TRY(OkIf(lastObjectStoreId != INT64_MAX), NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR, IDB_REPORT_INTERNAL_ERR_LAMBDA); QM_TRY(OkIf(lastIndexId != INT64_MAX), NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR, IDB_REPORT_INTERNAL_ERR_LAMBDA); mMetadata->mNextObjectStoreId = lastObjectStoreId + 1; mMetadata->mNextIndexId = lastIndexId + 1; return NS_OK; } /* static */ nsresult OpenDatabaseOp::UpdateLocaleAwareIndex( mozIStorageConnection& aConnection, const IndexMetadata& aIndexMetadata, const nsCString& aLocale) { const auto indexTable = aIndexMetadata.unique() ? "unique_index_data"_ns : "index_data"_ns; // The parameter names are not used, parameters are bound by index only // locally in the same function. const nsCString readQuery = "SELECT value, object_data_key FROM "_ns + indexTable + " WHERE index_id = :index_id"_ns; QM_TRY_INSPECT(const auto& readStmt, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( nsCOMPtr, aConnection, CreateStatement, readQuery)); QM_TRY(MOZ_TO_RESULT(readStmt->BindInt64ByIndex(0, aIndexMetadata.id()))); QM_TRY(CollectWhileHasResult( *readStmt, [&aConnection, &indexTable, &aIndexMetadata, &aLocale, writeStmt = nsCOMPtr{}]( auto& readStmt) mutable -> mozilla::Result { if (!writeStmt) { QM_TRY_UNWRAP( writeStmt, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( nsCOMPtr, aConnection, CreateStatement, "UPDATE "_ns + indexTable + "SET value_locale = :"_ns + kStmtParamNameValueLocale + " WHERE index_id = :"_ns + kStmtParamNameIndexId + " AND value = :"_ns + kStmtParamNameValue + " AND object_data_key = :"_ns + kStmtParamNameObjectDataKey)); } mozStorageStatementScoper scoper(writeStmt); QM_TRY(MOZ_TO_RESULT(writeStmt->BindInt64ByName(kStmtParamNameIndexId, aIndexMetadata.id()))); Key oldKey, objectStorePosition; QM_TRY(MOZ_TO_RESULT(oldKey.SetFromStatement(&readStmt, 0))); QM_TRY(MOZ_TO_RESULT( oldKey.BindToStatement(writeStmt, kStmtParamNameValue))); QM_TRY_INSPECT(const auto& newSortKey, oldKey.ToLocaleAwareKey(aLocale)); QM_TRY(MOZ_TO_RESULT( newSortKey.BindToStatement(writeStmt, kStmtParamNameValueLocale))); QM_TRY( MOZ_TO_RESULT(objectStorePosition.SetFromStatement(&readStmt, 1))); QM_TRY(MOZ_TO_RESULT(objectStorePosition.BindToStatement( writeStmt, kStmtParamNameObjectDataKey))); QM_TRY(MOZ_TO_RESULT(writeStmt->Execute())); return Ok{}; })); // The parameter names are not used, parameters are bound by index only // locally in the same function. static constexpr auto metaQuery = "UPDATE object_store_index SET " "locale = :locale WHERE id = :id"_ns; QM_TRY_INSPECT(const auto& metaStmt, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( nsCOMPtr, aConnection, CreateStatement, metaQuery)); QM_TRY(MOZ_TO_RESULT( metaStmt->BindStringByIndex(0, NS_ConvertASCIItoUTF16(aLocale)))); QM_TRY(MOZ_TO_RESULT(metaStmt->BindInt64ByIndex(1, aIndexMetadata.id()))); QM_TRY(MOZ_TO_RESULT(metaStmt->Execute())); return NS_OK; } nsresult OpenDatabaseOp::BeginVersionChange() { AssertIsOnOwningThread(); MOZ_ASSERT(mState == State::BeginVersionChange); MOZ_ASSERT(mMaybeBlockedDatabases.IsEmpty()); MOZ_ASSERT(mMetadata->mCommonMetadata.version() <= mRequestedVersion); MOZ_ASSERT(!mDatabase); MOZ_ASSERT(!mVersionChangeTransaction); if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || IsActorDestroyed()) { IDB_REPORT_INTERNAL_ERR(); QM_TRY(MOZ_TO_RESULT(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR)); } EnsureDatabaseActor(); if (mDatabase->IsInvalidated()) { IDB_REPORT_INTERNAL_ERR(); QM_TRY(MOZ_TO_RESULT(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR)); } MOZ_ASSERT(!mDatabase->IsClosed()); DatabaseActorInfo* info; MOZ_ALWAYS_TRUE(gLiveDatabaseHashtable->Get(mDatabaseId, &info)); MOZ_ASSERT(info->mLiveDatabases.Contains(mDatabase.unsafeGetRawPtr())); MOZ_ASSERT(!info->mWaitingFactoryOp); MOZ_ASSERT(info->mMetadata == mMetadata); auto transaction = MakeSafeRefPtr(this); if (NS_WARN_IF(!transaction->CopyDatabaseMetadata())) { return NS_ERROR_OUT_OF_MEMORY; } MOZ_ASSERT(info->mMetadata != mMetadata); mMetadata = info->mMetadata.clonePtr(); const Maybe newVersion = Some(mRequestedVersion); QM_TRY(MOZ_TO_RESULT(SendVersionChangeMessages( info, mDatabase.maybeDeref(), mMetadata->mCommonMetadata.version(), newVersion))); mVersionChangeTransaction = std::move(transaction); if (mMaybeBlockedDatabases.IsEmpty()) { // We don't need to wait on any databases, just jump to the transaction // pool. WaitForTransactions(); return NS_OK; } // If the actor gets destroyed, mWaitingFactoryOp will hold the last strong // reference to us. info->mWaitingFactoryOp = this; mState = State::WaitingForOtherDatabasesToClose; return NS_OK; } bool OpenDatabaseOp::AreActorsAlive() { AssertIsOnOwningThread(); MOZ_ASSERT(mDatabase); return !(IsActorDestroyed() || mDatabase->IsActorDestroyed()); } void OpenDatabaseOp::SendBlockedNotification() { AssertIsOnOwningThread(); MOZ_ASSERT(mState == State::WaitingForOtherDatabasesToClose); if (!IsActorDestroyed()) { Unused << SendBlocked(mMetadata->mCommonMetadata.version()); } } nsresult OpenDatabaseOp::DispatchToWorkThread() { AssertIsOnOwningThread(); MOZ_ASSERT(mState == State::WaitingForTransactionsToComplete); MOZ_ASSERT(mVersionChangeTransaction); MOZ_ASSERT(mVersionChangeTransaction->GetMode() == IDBTransaction::Mode::VersionChange); MOZ_ASSERT(mMaybeBlockedDatabases.IsEmpty()); if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || IsActorDestroyed() || mDatabase->IsInvalidated()) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } mState = State::DatabaseWorkVersionChange; // Intentionally empty. nsTArray objectStoreNames; const int64_t loggingSerialNumber = mVersionChangeTransaction->LoggingSerialNumber(); const nsID& backgroundChildLoggingId = mVersionChangeTransaction->GetLoggingInfo()->Id(); if (NS_WARN_IF(!mDatabase->RegisterTransaction(*mVersionChangeTransaction))) { return NS_ERROR_OUT_OF_MEMORY; } if (!gConnectionPool) { gConnectionPool = new ConnectionPool(); } RefPtr versionChangeOp = new VersionChangeOp(this); uint64_t transactionId = versionChangeOp->StartOnConnectionPool( backgroundChildLoggingId, mVersionChangeTransaction->DatabaseId(), loggingSerialNumber, objectStoreNames, /* aIsWriteTransaction */ true); mVersionChangeOp = versionChangeOp; mVersionChangeTransaction->NoteActiveRequest(); mVersionChangeTransaction->Init(transactionId); return NS_OK; } nsresult OpenDatabaseOp::SendUpgradeNeeded() { AssertIsOnOwningThread(); MOZ_ASSERT(mState == State::DatabaseWorkVersionChange); MOZ_ASSERT(mVersionChangeTransaction); MOZ_ASSERT(mMaybeBlockedDatabases.IsEmpty()); MOZ_ASSERT(!HasFailed()); MOZ_ASSERT_IF(!IsActorDestroyed(), mDatabase); if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || IsActorDestroyed()) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } const SafeRefPtr transaction = std::move(mVersionChangeTransaction); nsresult rv = EnsureDatabaseActorIsAlive(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Transfer ownership to IPDL. transaction->SetActorAlive(); if (!mDatabase->SendPBackgroundIDBVersionChangeTransactionConstructor( transaction.unsafeGetRawPtr(), mMetadata->mCommonMetadata.version(), mRequestedVersion, mMetadata->mNextObjectStoreId, mMetadata->mNextIndexId)) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } return NS_OK; } void OpenDatabaseOp::SendResults() { AssertIsOnOwningThread(); MOZ_ASSERT(mState == State::SendingResults); MOZ_ASSERT(mMaybeBlockedDatabases.IsEmpty()); MOZ_ASSERT_IF(!HasFailed(), !mVersionChangeTransaction); DebugOnly info = nullptr; MOZ_ASSERT_IF( gLiveDatabaseHashtable && gLiveDatabaseHashtable->Get(mDatabaseId, &info), !info->mWaitingFactoryOp); if (mVersionChangeTransaction) { MOZ_ASSERT(HasFailed()); mVersionChangeTransaction->Abort(ResultCode(), /* aForce */ true); mVersionChangeTransaction = nullptr; } if (IsActorDestroyed()) { SetFailureCodeIfUnset(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); } else { FactoryRequestResponse response; if (!HasFailed()) { // If we just successfully completed a versionchange operation then we // need to update the version in our metadata. mMetadata->mCommonMetadata.version() = mRequestedVersion; nsresult rv = EnsureDatabaseActorIsAlive(); if (NS_SUCCEEDED(rv)) { // We successfully opened a database so use its actor as the success // result for this request. // XXX OpenDatabaseRequestResponse stores a raw pointer, can this be // avoided? response = OpenDatabaseRequestResponse{ WrapNotNull(mDatabase.unsafeGetRawPtr())}; } else { response = ClampResultCode(rv); #ifdef DEBUG SetFailureCode(response.get_nsresult()); #endif } } else { #ifdef DEBUG // If something failed then our metadata pointer is now bad. No one should // ever touch it again though so just null it out in DEBUG builds to make // sure we find such cases. mMetadata = nullptr; #endif response = ClampResultCode(ResultCode()); } Unused << PBackgroundIDBFactoryRequestParent::Send__delete__(this, response); } if (mDatabase) { MOZ_ASSERT(!mDirectoryLock); if (HasFailed()) { mDatabase->Invalidate(); } // Make sure to release the database on this thread. mDatabase = nullptr; CleanupMetadata(); } else if (mDirectoryLock) { // ConnectionClosedCallback will call CleanupMetadata(). nsCOMPtr callback = NewRunnableMethod( "dom::indexedDB::OpenDatabaseOp::ConnectionClosedCallback", this, &OpenDatabaseOp::ConnectionClosedCallback); RefPtr helper = new WaitForTransactionsHelper(mDatabaseId, callback); helper->WaitForTransactions(); } else { CleanupMetadata(); } FinishSendResults(); } void OpenDatabaseOp::ConnectionClosedCallback() { AssertIsOnOwningThread(); MOZ_ASSERT(HasFailed()); MOZ_ASSERT(mDirectoryLock); mDirectoryLock = nullptr; CleanupMetadata(); } void OpenDatabaseOp::EnsureDatabaseActor() { AssertIsOnOwningThread(); MOZ_ASSERT(mState == State::BeginVersionChange || mState == State::DatabaseWorkVersionChange || mState == State::SendingResults); MOZ_ASSERT(!HasFailed()); MOZ_ASSERT(!mDatabaseFilePath.IsEmpty()); MOZ_ASSERT(!IsActorDestroyed()); if (mDatabase) { return; } MOZ_ASSERT(mMetadata->mDatabaseId.IsEmpty()); mMetadata->mDatabaseId = mDatabaseId; MOZ_ASSERT(mMetadata->mFilePath.IsEmpty()); mMetadata->mFilePath = mDatabaseFilePath; DatabaseActorInfo* info; if (gLiveDatabaseHashtable->Get(mDatabaseId, &info)) { AssertMetadataConsistency(*info->mMetadata); mMetadata = info->mMetadata.clonePtr(); } Maybe maybeKey = mInPrivateBrowsing ? mFileManager->MutableCipherKeyManagerRef().Get() : Nothing(); MOZ_RELEASE_ASSERT(mInPrivateBrowsing == maybeKey.isSome()); // XXX Shouldn't Manager() return already_AddRefed when // PBackgroundIDBFactoryParent is declared refcounted? mDatabase = MakeSafeRefPtr( SafeRefPtr{static_cast(Manager()), AcquireStrongRefFromRawPtr{}}, mCommonParams.principalInfo(), mContentParentId, mOriginMetadata, mTelemetryId, mMetadata.clonePtr(), mFileManager.clonePtr(), std::move(mDirectoryLock), mInPrivateBrowsing, maybeKey); if (info) { info->mLiveDatabases.AppendElement( WrapNotNullUnchecked(mDatabase.unsafeGetRawPtr())); } else { // XXX Maybe use LookupOrInsertWith above, to avoid a second lookup here? info = gLiveDatabaseHashtable ->InsertOrUpdate( mDatabaseId, MakeUnique( mMetadata.clonePtr(), WrapNotNullUnchecked(mDatabase.unsafeGetRawPtr()))) .get(); } // Balanced in Database::CleanupMetadata(). IncreaseBusyCount(); } nsresult OpenDatabaseOp::EnsureDatabaseActorIsAlive() { AssertIsOnOwningThread(); MOZ_ASSERT(mState == State::DatabaseWorkVersionChange || mState == State::SendingResults); MOZ_ASSERT(!HasFailed()); MOZ_ASSERT(!IsActorDestroyed()); EnsureDatabaseActor(); if (mDatabase->IsActorAlive()) { return NS_OK; } auto* const factory = static_cast(Manager()); QM_TRY_INSPECT(const auto& spec, MetadataToSpec()); mDatabase->SetActorAlive(); if (!factory->SendPBackgroundIDBDatabaseConstructor( mDatabase.unsafeGetRawPtr(), spec, WrapNotNull(this))) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } return NS_OK; } Result OpenDatabaseOp::MetadataToSpec() const { AssertIsOnOwningThread(); MOZ_ASSERT(mMetadata); DatabaseSpec spec; spec.metadata() = mMetadata->mCommonMetadata; QM_TRY_UNWRAP(spec.objectStores(), TransformIntoNewArrayAbortOnErr( mMetadata->mObjectStores, [](const auto& objectStoreEntry) -> mozilla::Result { FullObjectStoreMetadata* metadata = objectStoreEntry.GetWeak(); MOZ_ASSERT(objectStoreEntry.GetKey()); MOZ_ASSERT(metadata); ObjectStoreSpec objectStoreSpec; objectStoreSpec.metadata() = metadata->mCommonMetadata; QM_TRY_UNWRAP(auto indexes, TransformIntoNewArray( metadata->mIndexes, [](const auto& indexEntry) { FullIndexMetadata* indexMetadata = indexEntry.GetWeak(); MOZ_ASSERT(indexEntry.GetKey()); MOZ_ASSERT(indexMetadata); return indexMetadata->mCommonMetadata; }, fallible)); objectStoreSpec.indexes() = std::move(indexes); return objectStoreSpec; }, fallible)); return spec; } #ifdef DEBUG void OpenDatabaseOp::AssertMetadataConsistency( const FullDatabaseMetadata& aMetadata) { AssertIsOnBackgroundThread(); const FullDatabaseMetadata& thisDB = *mMetadata; const FullDatabaseMetadata& otherDB = aMetadata; MOZ_ASSERT(&thisDB != &otherDB); MOZ_ASSERT(thisDB.mCommonMetadata.name() == otherDB.mCommonMetadata.name()); MOZ_ASSERT(thisDB.mCommonMetadata.version() == otherDB.mCommonMetadata.version()); MOZ_ASSERT(thisDB.mCommonMetadata.persistenceType() == otherDB.mCommonMetadata.persistenceType()); MOZ_ASSERT(thisDB.mDatabaseId == otherDB.mDatabaseId); MOZ_ASSERT(thisDB.mFilePath == otherDB.mFilePath); // |thisDB| reflects the latest objectStore and index ids that have committed // to disk. The in-memory metadata |otherDB| keeps track of objectStores and // indexes that were created and then removed as well, so the next ids for // |otherDB| may be higher than for |thisDB|. MOZ_ASSERT(thisDB.mNextObjectStoreId <= otherDB.mNextObjectStoreId); MOZ_ASSERT(thisDB.mNextIndexId <= otherDB.mNextIndexId); MOZ_ASSERT(thisDB.mObjectStores.Count() == otherDB.mObjectStores.Count()); for (const auto& thisObjectStore : thisDB.mObjectStores.Values()) { MOZ_ASSERT(thisObjectStore); MOZ_ASSERT(!thisObjectStore->mDeleted); auto otherObjectStore = MatchMetadataNameOrId( otherDB.mObjectStores, thisObjectStore->mCommonMetadata.id()); MOZ_ASSERT(otherObjectStore); MOZ_ASSERT(thisObjectStore != &otherObjectStore.ref()); MOZ_ASSERT(thisObjectStore->mCommonMetadata.id() == otherObjectStore->mCommonMetadata.id()); MOZ_ASSERT(thisObjectStore->mCommonMetadata.name() == otherObjectStore->mCommonMetadata.name()); MOZ_ASSERT(thisObjectStore->mCommonMetadata.autoIncrement() == otherObjectStore->mCommonMetadata.autoIncrement()); MOZ_ASSERT(thisObjectStore->mCommonMetadata.keyPath() == otherObjectStore->mCommonMetadata.keyPath()); // mNextAutoIncrementId and mCommittedAutoIncrementId may be modified // concurrently with this OpenOp, so it is not possible to assert equality // here. It's also possible that we've written the new ids to disk but not // yet updated the in-memory count. // TODO The first part of the comment should probably be rephrased. I think // it still applies but it sounds as if this were thread-unsafe like it was // before, which isn't true anymore. { const auto&& thisAutoIncrementIds = thisObjectStore->mAutoIncrementIds.Lock(); const auto&& otherAutoIncrementIds = otherObjectStore->mAutoIncrementIds.Lock(); MOZ_ASSERT(thisAutoIncrementIds->next <= otherAutoIncrementIds->next); MOZ_ASSERT( thisAutoIncrementIds->committed <= otherAutoIncrementIds->committed || thisAutoIncrementIds->committed == otherAutoIncrementIds->next); } MOZ_ASSERT(!otherObjectStore->mDeleted); MOZ_ASSERT(thisObjectStore->mIndexes.Count() == otherObjectStore->mIndexes.Count()); for (const auto& thisIndex : thisObjectStore->mIndexes.Values()) { MOZ_ASSERT(thisIndex); MOZ_ASSERT(!thisIndex->mDeleted); auto otherIndex = MatchMetadataNameOrId(otherObjectStore->mIndexes, thisIndex->mCommonMetadata.id()); MOZ_ASSERT(otherIndex); MOZ_ASSERT(thisIndex != &otherIndex.ref()); MOZ_ASSERT(thisIndex->mCommonMetadata.id() == otherIndex->mCommonMetadata.id()); MOZ_ASSERT(thisIndex->mCommonMetadata.name() == otherIndex->mCommonMetadata.name()); MOZ_ASSERT(thisIndex->mCommonMetadata.keyPath() == otherIndex->mCommonMetadata.keyPath()); MOZ_ASSERT(thisIndex->mCommonMetadata.unique() == otherIndex->mCommonMetadata.unique()); MOZ_ASSERT(thisIndex->mCommonMetadata.multiEntry() == otherIndex->mCommonMetadata.multiEntry()); MOZ_ASSERT(!otherIndex->mDeleted); } } } #endif // DEBUG nsresult OpenDatabaseOp::VersionChangeOp::DoDatabaseWork( DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(mOpenDatabaseOp); MOZ_ASSERT(mOpenDatabaseOp->mState == State::DatabaseWorkVersionChange); if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || !OperationMayProceed()) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } AUTO_PROFILER_LABEL("OpenDatabaseOp::VersionChangeOp::DoDatabaseWork", DOM); IDB_LOG_MARK_PARENT_TRANSACTION("Beginning database work", "DB Start", IDB_LOG_ID_STRING(mBackgroundChildLoggingId), mTransactionLoggingSerialNumber); Transaction().SetActiveOnConnectionThread(); QM_TRY(MOZ_TO_RESULT(aConnection->BeginWriteTransaction())); // The parameter names are not used, parameters are bound by index only // locally in the same function. QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement( "UPDATE database SET version = :version;"_ns, ([&self = *this]( mozIStorageStatement& updateStmt) -> mozilla::Result { QM_TRY(MOZ_TO_RESULT( updateStmt.BindInt64ByIndex(0, int64_t(self.mRequestedVersion)))); return Ok{}; })))); return NS_OK; } nsresult OpenDatabaseOp::VersionChangeOp::SendSuccessResult() { AssertIsOnOwningThread(); MOZ_ASSERT(mOpenDatabaseOp); MOZ_ASSERT(mOpenDatabaseOp->mState == State::DatabaseWorkVersionChange); MOZ_ASSERT(mOpenDatabaseOp->mVersionChangeOp == this); nsresult rv = mOpenDatabaseOp->SendUpgradeNeeded(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } bool OpenDatabaseOp::VersionChangeOp::SendFailureResult(nsresult aResultCode) { AssertIsOnOwningThread(); MOZ_ASSERT(mOpenDatabaseOp); MOZ_ASSERT(mOpenDatabaseOp->mState == State::DatabaseWorkVersionChange); MOZ_ASSERT(mOpenDatabaseOp->mVersionChangeOp == this); mOpenDatabaseOp->SetFailureCode(aResultCode); mOpenDatabaseOp->mState = State::SendingResults; MOZ_ALWAYS_SUCCEEDS(mOpenDatabaseOp->Run()); return false; } void OpenDatabaseOp::VersionChangeOp::Cleanup() { AssertIsOnOwningThread(); MOZ_ASSERT(mOpenDatabaseOp); MOZ_ASSERT(mOpenDatabaseOp->mVersionChangeOp == this); mOpenDatabaseOp->mVersionChangeOp = nullptr; mOpenDatabaseOp = nullptr; #ifdef DEBUG // A bit hacky but the VersionChangeOp is not generated in response to a // child request like most other database operations. Do this to make our // assertions happy. // // XXX: Depending on timing, in most cases, NoteActorDestroyed will not have // been destroyed before, but in some cases it has. This should be reworked in // a way this hack is not necessary. There are also several similar cases in // other *Op classes. if (!IsActorDestroyed()) { NoteActorDestroyed(); } #endif TransactionDatabaseOperationBase::Cleanup(); } void DeleteDatabaseOp::LoadPreviousVersion(nsIFile& aDatabaseFile) { AssertIsOnIOThread(); MOZ_ASSERT(mState == State::DatabaseWorkOpen); MOZ_ASSERT(!mPreviousVersion); AUTO_PROFILER_LABEL("DeleteDatabaseOp::LoadPreviousVersion", DOM); nsresult rv; nsCOMPtr ss = do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID, &rv); if (NS_WARN_IF(NS_FAILED(rv))) { return; } IndexedDatabaseManager* const idm = IndexedDatabaseManager::Get(); MOZ_ASSERT(idm); const PersistenceType persistenceType = mCommonParams.metadata().persistenceType(); const nsAString& databaseName = mCommonParams.metadata().name(); SafeRefPtr fileManager = idm->GetFileManager( persistenceType, mOriginMetadata.mOrigin, databaseName); if (!fileManager) { fileManager = MakeSafeRefPtr( persistenceType, mOriginMetadata, databaseName, mDatabaseId, mEnforcingQuota, mInPrivateBrowsing); } const auto maybeKey = mInPrivateBrowsing ? Some(fileManager->MutableCipherKeyManagerRef().Ensure()) : Nothing(); MOZ_RELEASE_ASSERT(mInPrivateBrowsing == maybeKey.isSome()); // Pass -1 as the directoryLockId to disable quota checking, since we might // temporarily exceed quota before deleting the database. QM_TRY_INSPECT(const auto& dbFileUrl, GetDatabaseFileURL(aDatabaseFile, -1, maybeKey), QM_VOID); QM_TRY_UNWRAP(const NotNull> connection, OpenDatabaseAndHandleBusy(*ss, *dbFileUrl), QM_VOID); #ifdef DEBUG { QM_TRY_INSPECT(const auto& stmt, CreateAndExecuteSingleStepStatement< SingleStepResult::ReturnNullIfNoResult>( *connection, "SELECT name FROM database"_ns), QM_VOID); QM_TRY(OkIf(stmt), QM_VOID); nsString databaseName; rv = stmt->GetString(0, databaseName); if (NS_WARN_IF(NS_FAILED(rv))) { return; } MOZ_ASSERT(mCommonParams.metadata().name() == databaseName); } #endif QM_TRY_INSPECT(const auto& stmt, CreateAndExecuteSingleStepStatement< SingleStepResult::ReturnNullIfNoResult>( *connection, "SELECT version FROM database"_ns), QM_VOID); QM_TRY(OkIf(stmt), QM_VOID); int64_t version; rv = stmt->GetInt64(0, &version); if (NS_WARN_IF(NS_FAILED(rv))) { return; } mPreviousVersion = uint64_t(version); } nsresult DeleteDatabaseOp::DatabaseOpen() { AssertIsOnOwningThread(); MOZ_ASSERT(mState == State::DatabaseOpenPending); nsresult rv = SendToIOThread(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } nsresult DeleteDatabaseOp::DoDatabaseWork() { AssertIsOnIOThread(); MOZ_ASSERT(mState == State::DatabaseWorkOpen); MOZ_ASSERT(mOriginMetadata.mPersistenceType == mCommonParams.metadata().persistenceType()); AUTO_PROFILER_LABEL("DeleteDatabaseOp::DoDatabaseWork", DOM); if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || !OperationMayProceed()) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } const nsAString& databaseName = mCommonParams.metadata().name(); QuotaManager* const quotaManager = QuotaManager::Get(); MOZ_ASSERT(quotaManager); QM_TRY_UNWRAP(auto directory, quotaManager->GetOriginDirectory(mOriginMetadata)); QM_TRY(MOZ_TO_RESULT( directory->Append(NS_LITERAL_STRING_FROM_CSTRING(IDB_DIRECTORY_NAME)))); QM_TRY_UNWRAP(mDatabaseDirectoryPath, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( nsString, directory, GetPath)); mDatabaseFilenameBase = GetDatabaseFilenameBase(databaseName, mOriginMetadata.mIsPrivate); QM_TRY_INSPECT( const auto& dbFile, CloneFileAndAppend(*directory, mDatabaseFilenameBase + kSQLiteSuffix)); #ifdef DEBUG { QM_TRY_INSPECT( const auto& databaseFilePath, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, dbFile, GetPath)); MOZ_ASSERT(databaseFilePath == mDatabaseFilePath); } #endif QM_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE_MEMBER(dbFile, Exists)); if (exists) { // Parts of this function may fail but that shouldn't prevent us from // deleting the file eventually. LoadPreviousVersion(*dbFile); mState = State::BeginVersionChange; } else { mState = State::SendingResults; } QM_TRY(MOZ_TO_RESULT(mOwningEventTarget->Dispatch(this, NS_DISPATCH_NORMAL))); return NS_OK; } nsresult DeleteDatabaseOp::BeginVersionChange() { AssertIsOnOwningThread(); MOZ_ASSERT(mState == State::BeginVersionChange); MOZ_ASSERT(mMaybeBlockedDatabases.IsEmpty()); if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || IsActorDestroyed()) { IDB_REPORT_INTERNAL_ERR(); QM_TRY(MOZ_TO_RESULT(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR)); } DatabaseActorInfo* info; if (gLiveDatabaseHashtable->Get(mDatabaseId, &info)) { MOZ_ASSERT(!info->mWaitingFactoryOp); nsresult rv = SendVersionChangeMessages(info, Nothing(), mPreviousVersion, Nothing()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (!mMaybeBlockedDatabases.IsEmpty()) { // If the actor gets destroyed, mWaitingFactoryOp will hold the last // strong reference to us. info->mWaitingFactoryOp = this; mState = State::WaitingForOtherDatabasesToClose; return NS_OK; } } // No other databases need to be notified, just make sure that all // transactions are complete. WaitForTransactions(); return NS_OK; } bool DeleteDatabaseOp::AreActorsAlive() { AssertIsOnOwningThread(); return !IsActorDestroyed(); } nsresult DeleteDatabaseOp::DispatchToWorkThread() { AssertIsOnOwningThread(); MOZ_ASSERT(mState == State::WaitingForTransactionsToComplete); MOZ_ASSERT(mMaybeBlockedDatabases.IsEmpty()); if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || IsActorDestroyed()) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } mState = State::DatabaseWorkVersionChange; RefPtr versionChangeOp = new VersionChangeOp(this); QuotaManager* const quotaManager = QuotaManager::Get(); MOZ_ASSERT(quotaManager); nsresult rv = quotaManager->IOThread()->Dispatch(versionChangeOp.forget(), NS_DISPATCH_NORMAL); if (NS_WARN_IF(NS_FAILED(rv))) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } return NS_OK; } void DeleteDatabaseOp::SendBlockedNotification() { AssertIsOnOwningThread(); MOZ_ASSERT(mState == State::WaitingForOtherDatabasesToClose); if (!IsActorDestroyed()) { Unused << SendBlocked(mPreviousVersion); } } void DeleteDatabaseOp::SendResults() { AssertIsOnOwningThread(); MOZ_ASSERT(mState == State::SendingResults); MOZ_ASSERT(mMaybeBlockedDatabases.IsEmpty()); DebugOnly info = nullptr; MOZ_ASSERT_IF( gLiveDatabaseHashtable && gLiveDatabaseHashtable->Get(mDatabaseId, &info), !info->mWaitingFactoryOp); if (!IsActorDestroyed()) { FactoryRequestResponse response; if (!HasFailed()) { response = DeleteDatabaseRequestResponse(mPreviousVersion); } else { response = ClampResultCode(ResultCode()); } Unused << PBackgroundIDBFactoryRequestParent::Send__delete__(this, response); } mDirectoryLock = nullptr; CleanupMetadata(); FinishSendResults(); } nsresult DeleteDatabaseOp::VersionChangeOp::RunOnIOThread() { AssertIsOnIOThread(); MOZ_ASSERT(mDeleteDatabaseOp->mState == State::DatabaseWorkVersionChange); AUTO_PROFILER_LABEL("DeleteDatabaseOp::VersionChangeOp::RunOnIOThread", DOM); if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || !OperationMayProceed()) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } const PersistenceType& persistenceType = mDeleteDatabaseOp->mCommonParams.metadata().persistenceType(); QuotaManager* quotaManager = mDeleteDatabaseOp->mEnforcingQuota ? QuotaManager::Get() : nullptr; MOZ_ASSERT_IF(mDeleteDatabaseOp->mEnforcingQuota, quotaManager); nsCOMPtr directory = GetFileForPath(mDeleteDatabaseOp->mDatabaseDirectoryPath); if (NS_WARN_IF(!directory)) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } nsresult rv = RemoveDatabaseFilesAndDirectory( *directory, mDeleteDatabaseOp->mDatabaseFilenameBase, quotaManager, persistenceType, mDeleteDatabaseOp->mOriginMetadata, mDeleteDatabaseOp->mCommonParams.metadata().name()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = mOwningEventTarget->Dispatch(this, NS_DISPATCH_NORMAL); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } void DeleteDatabaseOp::VersionChangeOp::RunOnOwningThread() { AssertIsOnOwningThread(); MOZ_ASSERT(mDeleteDatabaseOp->mState == State::DatabaseWorkVersionChange); const RefPtr deleteOp = std::move(mDeleteDatabaseOp); if (deleteOp->IsActorDestroyed()) { IDB_REPORT_INTERNAL_ERR(); deleteOp->SetFailureCode(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); } else if (HasFailed()) { deleteOp->SetFailureCodeIfUnset(ResultCode()); } else { DatabaseActorInfo* info; // Inform all the other databases that they are now invalidated. That // should remove the previous metadata from our table. if (gLiveDatabaseHashtable->Get(deleteOp->mDatabaseId, &info)) { MOZ_ASSERT(!info->mLiveDatabases.IsEmpty()); MOZ_ASSERT(!info->mWaitingFactoryOp); nsTArray> liveDatabases; if (NS_WARN_IF(!liveDatabases.SetCapacity(info->mLiveDatabases.Length(), fallible))) { deleteOp->SetFailureCode(NS_ERROR_OUT_OF_MEMORY); } else { std::transform(info->mLiveDatabases.cbegin(), info->mLiveDatabases.cend(), MakeBackInserter(liveDatabases), [](const auto& aDatabase) -> SafeRefPtr { return {aDatabase.get(), AcquireStrongRefFromRawPtr{}}; }); #ifdef DEBUG // The code below should result in the deletion of |info|. Set to null // here to make sure we find invalid uses later. info = nullptr; #endif for (const auto& database : liveDatabases) { database->Invalidate(); } MOZ_ASSERT(!gLiveDatabaseHashtable->Get(deleteOp->mDatabaseId)); } } } // We hold a strong ref to the deleteOp, so it's safe to call Run() directly. deleteOp->mState = State::SendingResults; MOZ_ALWAYS_SUCCEEDS(deleteOp->Run()); #ifdef DEBUG // A bit hacky but the DeleteDatabaseOp::VersionChangeOp is not really a // normal database operation that is tied to an actor. Do this to make our // assertions happy. NoteActorDestroyed(); #endif } nsresult DeleteDatabaseOp::VersionChangeOp::Run() { nsresult rv; if (IsOnIOThread()) { rv = RunOnIOThread(); } else { RunOnOwningThread(); rv = NS_OK; } if (NS_WARN_IF(NS_FAILED(rv))) { SetFailureCodeIfUnset(rv); MOZ_ALWAYS_SUCCEEDS(mOwningEventTarget->Dispatch(this, NS_DISPATCH_NORMAL)); } return NS_OK; } TransactionDatabaseOperationBase::TransactionDatabaseOperationBase( SafeRefPtr aTransaction) : DatabaseOperationBase(aTransaction->GetLoggingInfo()->Id(), aTransaction->GetLoggingInfo()->NextRequestSN()), mTransaction(WrapNotNull(std::move(aTransaction))), mTransactionIsAborted((*mTransaction)->IsAborted()), mTransactionLoggingSerialNumber((*mTransaction)->LoggingSerialNumber()) { MOZ_ASSERT(LoggingSerialNumber()); } TransactionDatabaseOperationBase::TransactionDatabaseOperationBase( SafeRefPtr aTransaction, uint64_t aLoggingSerialNumber) : DatabaseOperationBase(aTransaction->GetLoggingInfo()->Id(), aLoggingSerialNumber), mTransaction(WrapNotNull(std::move(aTransaction))), mTransactionIsAborted((*mTransaction)->IsAborted()), mTransactionLoggingSerialNumber((*mTransaction)->LoggingSerialNumber()) {} TransactionDatabaseOperationBase::~TransactionDatabaseOperationBase() { MOZ_ASSERT(mInternalState == InternalState::Completed); MOZ_ASSERT(!mTransaction, "TransactionDatabaseOperationBase::Cleanup() was not called by a " "subclass!"); } #ifdef DEBUG void TransactionDatabaseOperationBase::AssertIsOnConnectionThread() const { (*mTransaction)->AssertIsOnConnectionThread(); } #endif // DEBUG uint64_t TransactionDatabaseOperationBase::StartOnConnectionPool( const nsID& aBackgroundChildLoggingId, const nsACString& aDatabaseId, int64_t aLoggingSerialNumber, const nsTArray& aObjectStoreNames, bool aIsWriteTransaction) { AssertIsOnOwningThread(); MOZ_ASSERT(mInternalState == InternalState::Initial); // Must set mInternalState before dispatching otherwise we will race with the // connection thread. mInternalState = InternalState::DatabaseWork; return gConnectionPool->Start(aBackgroundChildLoggingId, aDatabaseId, aLoggingSerialNumber, aObjectStoreNames, aIsWriteTransaction, this); } void TransactionDatabaseOperationBase::DispatchToConnectionPool() { AssertIsOnOwningThread(); MOZ_ASSERT(mInternalState == InternalState::Initial); Unused << this->Run(); } void TransactionDatabaseOperationBase::RunOnConnectionThread() { MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(mInternalState == InternalState::DatabaseWork); MOZ_ASSERT(!HasFailed()); AUTO_PROFILER_LABEL("TransactionDatabaseOperationBase::RunOnConnectionThread", DOM); // There are several cases where we don't actually have to to any work here. if (mTransactionIsAborted || (*mTransaction)->IsInvalidatedOnAnyThread()) { // This transaction is already set to be aborted or invalidated. SetFailureCode(NS_ERROR_DOM_INDEXEDDB_ABORT_ERR); } else if (!OperationMayProceed()) { // The operation was canceled in some way, likely because the child process // has crashed. IDB_REPORT_INTERNAL_ERR(); OverrideFailureCode(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); } else { Database& database = (*mTransaction)->GetMutableDatabase(); // Here we're actually going to perform the database operation. nsresult rv = database.EnsureConnection(); if (NS_WARN_IF(NS_FAILED(rv))) { SetFailureCode(rv); } else { DatabaseConnection* connection = database.GetConnection(); MOZ_ASSERT(connection); auto& storageConnection = connection->MutableStorageConnection(); AutoSetProgressHandler autoProgress; if (mLoggingSerialNumber) { rv = autoProgress.Register(storageConnection, this); if (NS_WARN_IF(NS_FAILED(rv))) { SetFailureCode(rv); } } if (NS_SUCCEEDED(rv)) { if (mLoggingSerialNumber) { IDB_LOG_MARK_PARENT_TRANSACTION_REQUEST( "Beginning database work", "DB Start", IDB_LOG_ID_STRING(mBackgroundChildLoggingId), mTransactionLoggingSerialNumber, mLoggingSerialNumber); } rv = DoDatabaseWork(connection); if (mLoggingSerialNumber) { IDB_LOG_MARK_PARENT_TRANSACTION_REQUEST( "Finished database work", "DB End", IDB_LOG_ID_STRING(mBackgroundChildLoggingId), mTransactionLoggingSerialNumber, mLoggingSerialNumber); } if (NS_FAILED(rv)) { SetFailureCode(rv); } } } } // Must set mInternalState before dispatching otherwise we will race with the // owning thread. if (HasPreprocessInfo()) { mInternalState = InternalState::SendingPreprocess; } else { mInternalState = InternalState::SendingResults; } MOZ_ALWAYS_SUCCEEDS(mOwningEventTarget->Dispatch(this, NS_DISPATCH_NORMAL)); } bool TransactionDatabaseOperationBase::HasPreprocessInfo() { return false; } nsresult TransactionDatabaseOperationBase::SendPreprocessInfo() { return NS_OK; } void TransactionDatabaseOperationBase::NoteContinueReceived() { AssertIsOnOwningThread(); MOZ_ASSERT(mInternalState == InternalState::WaitingForContinue); mWaitingForContinue = false; mInternalState = InternalState::SendingResults; // This TransactionDatabaseOperationBase can only be held alive by the IPDL. // Run() can end up with clearing that last reference. So we need to add // a self reference here. RefPtr kungFuDeathGrip = this; Unused << this->Run(); } void TransactionDatabaseOperationBase::SendToConnectionPool() { AssertIsOnOwningThread(); MOZ_ASSERT(mInternalState == InternalState::Initial); // Must set mInternalState before dispatching otherwise we will race with the // connection thread. mInternalState = InternalState::DatabaseWork; gConnectionPool->Dispatch((*mTransaction)->TransactionId(), this); (*mTransaction)->NoteActiveRequest(); } void TransactionDatabaseOperationBase::SendPreprocess() { AssertIsOnOwningThread(); MOZ_ASSERT(mInternalState == InternalState::SendingPreprocess); SendPreprocessInfoOrResults(/* aSendPreprocessInfo */ true); } void TransactionDatabaseOperationBase::SendResults() { AssertIsOnOwningThread(); MOZ_ASSERT(mInternalState == InternalState::SendingResults); SendPreprocessInfoOrResults(/* aSendPreprocessInfo */ false); } void TransactionDatabaseOperationBase::SendPreprocessInfoOrResults( bool aSendPreprocessInfo) { AssertIsOnOwningThread(); MOZ_ASSERT(mInternalState == InternalState::SendingPreprocess || mInternalState == InternalState::SendingResults); // The flag is raised only when there is no mUpdateRefcountFunction for the // executing operation. It assume that is because the previous // StartTransactionOp was failed to begin a write transaction and it reported // when this operation has already jumped to the Connection thread. MOZ_DIAGNOSTIC_ASSERT_IF(mAssumingPreviousOperationFail, (*mTransaction)->IsAborted()); if (NS_WARN_IF(IsActorDestroyed())) { // Normally we wouldn't need to send any notifications if the actor was // already destroyed, but this can be a VersionChangeOp which needs to // notify its parent operation (OpenDatabaseOp) about the failure. // So SendFailureResult needs to be called even when the actor was // destroyed. Normal operations redundantly check if the actor was // destroyed in SendSuccessResult and SendFailureResult, therefore it's // ok to call it in all cases here. if (!HasFailed()) { IDB_REPORT_INTERNAL_ERR(); SetFailureCode(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); } } else if ((*mTransaction)->IsInvalidated() || (*mTransaction)->IsAborted()) { // Aborted transactions always see their requests fail with ABORT_ERR, // even if the request succeeded or failed with another error. OverrideFailureCode(NS_ERROR_DOM_INDEXEDDB_ABORT_ERR); } const nsresult rv = [aSendPreprocessInfo, this] { if (HasFailed()) { return ResultCode(); } if (aSendPreprocessInfo) { // This should not release the IPDL reference. return SendPreprocessInfo(); } // This may release the IPDL reference. return SendSuccessResult(); }(); if (NS_FAILED(rv)) { SetFailureCodeIfUnset(rv); // This should definitely release the IPDL reference. if (!SendFailureResult(rv)) { // Abort the transaction. (*mTransaction)->Abort(rv, /* aForce */ false); } } if (aSendPreprocessInfo && !HasFailed()) { mInternalState = InternalState::WaitingForContinue; mWaitingForContinue = true; } else { if (mLoggingSerialNumber) { (*mTransaction)->NoteFinishedRequest(mLoggingSerialNumber, ResultCode()); } Cleanup(); mInternalState = InternalState::Completed; } } bool TransactionDatabaseOperationBase::Init(TransactionBase& aTransaction) { AssertIsOnBackgroundThread(); MOZ_ASSERT(mInternalState == InternalState::Initial); return true; } void TransactionDatabaseOperationBase::Cleanup() { AssertIsOnOwningThread(); MOZ_ASSERT(mInternalState == InternalState::SendingResults); mTransaction.destroy(); } NS_IMETHODIMP TransactionDatabaseOperationBase::Run() { switch (mInternalState) { case InternalState::Initial: SendToConnectionPool(); return NS_OK; case InternalState::DatabaseWork: RunOnConnectionThread(); return NS_OK; case InternalState::SendingPreprocess: SendPreprocess(); return NS_OK; case InternalState::SendingResults: SendResults(); return NS_OK; default: MOZ_CRASH("Bad state!"); } } TransactionBase::CommitOp::CommitOp(SafeRefPtr aTransaction, nsresult aResultCode) : DatabaseOperationBase(aTransaction->GetLoggingInfo()->Id(), aTransaction->GetLoggingInfo()->NextRequestSN()), mTransaction(std::move(aTransaction)), mResultCode(aResultCode) { MOZ_ASSERT(mTransaction); MOZ_ASSERT(LoggingSerialNumber()); } nsresult TransactionBase::CommitOp::WriteAutoIncrementCounts() { MOZ_ASSERT(mTransaction); mTransaction->AssertIsOnConnectionThread(); MOZ_ASSERT(mTransaction->GetMode() == IDBTransaction::Mode::ReadWrite || mTransaction->GetMode() == IDBTransaction::Mode::ReadWriteFlush || mTransaction->GetMode() == IDBTransaction::Mode::Cleanup || mTransaction->GetMode() == IDBTransaction::Mode::VersionChange); const nsTArray>& metadataArray = mTransaction->mModifiedAutoIncrementObjectStoreMetadataArray; if (!metadataArray.IsEmpty()) { DatabaseConnection* connection = mTransaction->GetDatabase().GetConnection(); MOZ_ASSERT(connection); // The parameter names are not used, parameters are bound by index only // locally in the same function. auto stmt = DatabaseConnection::LazyStatement( *connection, "UPDATE object_store " "SET auto_increment = :auto_increment WHERE id " "= :object_store_id;"_ns); for (const auto& metadata : metadataArray) { MOZ_ASSERT(!metadata->mDeleted); const int64_t nextAutoIncrementId = [&metadata] { const auto&& lockedAutoIncrementIds = metadata->mAutoIncrementIds.Lock(); return lockedAutoIncrementIds->next; }(); MOZ_ASSERT(nextAutoIncrementId > 1); QM_TRY_INSPECT(const auto& borrowedStmt, stmt.Borrow()); QM_TRY(MOZ_TO_RESULT( borrowedStmt->BindInt64ByIndex(1, metadata->mCommonMetadata.id()))); QM_TRY(MOZ_TO_RESULT( borrowedStmt->BindInt64ByIndex(0, nextAutoIncrementId))); QM_TRY(MOZ_TO_RESULT(borrowedStmt->Execute())); } } return NS_OK; } void TransactionBase::CommitOp::CommitOrRollbackAutoIncrementCounts() { MOZ_ASSERT(mTransaction); mTransaction->AssertIsOnConnectionThread(); MOZ_ASSERT(mTransaction->GetMode() == IDBTransaction::Mode::ReadWrite || mTransaction->GetMode() == IDBTransaction::Mode::ReadWriteFlush || mTransaction->GetMode() == IDBTransaction::Mode::Cleanup || mTransaction->GetMode() == IDBTransaction::Mode::VersionChange); const auto& metadataArray = mTransaction->mModifiedAutoIncrementObjectStoreMetadataArray; if (!metadataArray.IsEmpty()) { bool committed = NS_SUCCEEDED(mResultCode); for (const auto& metadata : metadataArray) { auto&& lockedAutoIncrementIds = metadata->mAutoIncrementIds.Lock(); if (committed) { lockedAutoIncrementIds->committed = lockedAutoIncrementIds->next; } else { lockedAutoIncrementIds->next = lockedAutoIncrementIds->committed; } } } } #ifdef DEBUG void TransactionBase::CommitOp::AssertForeignKeyConsistency( DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); MOZ_ASSERT(mTransaction); mTransaction->AssertIsOnConnectionThread(); MOZ_ASSERT(mTransaction->GetMode() != IDBTransaction::Mode::ReadOnly); { QM_TRY_INSPECT( const auto& pragmaStmt, CreateAndExecuteSingleStepStatement( aConnection->MutableStorageConnection(), "PRAGMA foreign_keys;"_ns), QM_ASSERT_UNREACHABLE_VOID); int32_t foreignKeysEnabled; MOZ_ALWAYS_SUCCEEDS(pragmaStmt->GetInt32(0, &foreignKeysEnabled)); MOZ_ASSERT(foreignKeysEnabled, "Database doesn't have foreign keys enabled!"); } { QM_TRY_INSPECT(const bool& foreignKeyError, CreateAndExecuteSingleStepStatement< SingleStepResult::ReturnNullIfNoResult>( aConnection->MutableStorageConnection(), "PRAGMA foreign_key_check;"_ns), QM_ASSERT_UNREACHABLE_VOID); MOZ_ASSERT(!foreignKeyError, "Database has inconsisistent foreign keys!"); } } #endif // DEBUG NS_IMPL_ISUPPORTS_INHERITED0(TransactionBase::CommitOp, DatabaseOperationBase) NS_IMETHODIMP TransactionBase::CommitOp::Run() { MOZ_ASSERT(mTransaction); mTransaction->AssertIsOnConnectionThread(); AUTO_PROFILER_LABEL("TransactionBase::CommitOp::Run", DOM); IDB_LOG_MARK_PARENT_TRANSACTION_REQUEST( "Beginning database work", "DB Start", IDB_LOG_ID_STRING(mBackgroundChildLoggingId), mTransaction->LoggingSerialNumber(), mLoggingSerialNumber); if (mTransaction->GetMode() != IDBTransaction::Mode::ReadOnly && mTransaction->mHasBeenActiveOnConnectionThread) { if (DatabaseConnection* connection = mTransaction->GetDatabase().GetConnection()) { // May be null if the VersionChangeOp was canceled. DatabaseConnection::UpdateRefcountFunction* fileRefcountFunction = connection->GetUpdateRefcountFunction(); if (NS_SUCCEEDED(mResultCode)) { if (fileRefcountFunction) { mResultCode = fileRefcountFunction->WillCommit(); NS_WARNING_ASSERTION(NS_SUCCEEDED(mResultCode), "WillCommit() failed!"); } if (NS_SUCCEEDED(mResultCode)) { mResultCode = WriteAutoIncrementCounts(); NS_WARNING_ASSERTION(NS_SUCCEEDED(mResultCode), "WriteAutoIncrementCounts() failed!"); if (NS_SUCCEEDED(mResultCode)) { AssertForeignKeyConsistency(connection); mResultCode = connection->CommitWriteTransaction(); NS_WARNING_ASSERTION(NS_SUCCEEDED(mResultCode), "Commit failed!"); if (NS_SUCCEEDED(mResultCode) && mTransaction->GetMode() == IDBTransaction::Mode::ReadWriteFlush) { mResultCode = connection->Checkpoint(); } if (NS_SUCCEEDED(mResultCode) && fileRefcountFunction) { fileRefcountFunction->DidCommit(); } } } } if (NS_FAILED(mResultCode)) { if (fileRefcountFunction) { fileRefcountFunction->DidAbort(); } connection->RollbackWriteTransaction(); } CommitOrRollbackAutoIncrementCounts(); connection->FinishWriteTransaction(); if (mTransaction->GetMode() == IDBTransaction::Mode::Cleanup) { connection->DoIdleProcessing(/* aNeedsCheckpoint */ true); connection->EnableQuotaChecks(); } } } IDB_LOG_MARK_PARENT_TRANSACTION_REQUEST( "Finished database work", "DB End", IDB_LOG_ID_STRING(mBackgroundChildLoggingId), mTransaction->LoggingSerialNumber(), mLoggingSerialNumber); IDB_LOG_MARK_PARENT_TRANSACTION("Finished database work", "DB End", IDB_LOG_ID_STRING(mBackgroundChildLoggingId), mTransaction->LoggingSerialNumber()); return NS_OK; } void TransactionBase::CommitOp::TransactionFinishedBeforeUnblock() { AssertIsOnBackgroundThread(); MOZ_ASSERT(mTransaction); AUTO_PROFILER_LABEL("CommitOp::TransactionFinishedBeforeUnblock", DOM); if (!IsActorDestroyed()) { mTransaction->UpdateMetadata(mResultCode); } } void TransactionBase::CommitOp::TransactionFinishedAfterUnblock() { AssertIsOnBackgroundThread(); MOZ_ASSERT(mTransaction); IDB_LOG_MARK_PARENT_TRANSACTION( "Finished with result 0x%" PRIx32, "Transaction finished (0x%" PRIx32 ")", IDB_LOG_ID_STRING(mTransaction->GetLoggingInfo()->Id()), mTransaction->LoggingSerialNumber(), static_cast(mResultCode)); mTransaction->SendCompleteNotification(ClampResultCode(mResultCode)); mTransaction->GetMutableDatabase().UnregisterTransaction(*mTransaction); mTransaction = nullptr; #ifdef DEBUG // A bit hacky but the CommitOp is not really a normal database operation // that is tied to an actor. Do this to make our assertions happy. NoteActorDestroyed(); #endif } nsresult VersionChangeTransactionOp::SendSuccessResult() { AssertIsOnOwningThread(); // Nothing to send here, the API assumes that this request always succeeds. return NS_OK; } bool VersionChangeTransactionOp::SendFailureResult(nsresult aResultCode) { AssertIsOnOwningThread(); // The only option here is to cause the transaction to abort. return false; } void VersionChangeTransactionOp::Cleanup() { AssertIsOnOwningThread(); #ifdef DEBUG // A bit hacky but the VersionChangeTransactionOp is not generated in response // to a child request like most other database operations. Do this to make our // assertions happy. NoteActorDestroyed(); #endif TransactionDatabaseOperationBase::Cleanup(); } nsresult CreateObjectStoreOp::DoDatabaseWork(DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); AUTO_PROFILER_LABEL("CreateObjectStoreOp::DoDatabaseWork", DOM); #ifdef DEBUG { // Make sure that we're not creating an object store with the same name as // another that already exists. This should be impossible because we should // have thrown an error long before now... // The parameter names are not used, parameters are bound by index only // locally in the same function. QM_TRY_INSPECT(const bool& hasResult, aConnection ->BorrowAndExecuteSingleStepStatement( "SELECT name " "FROM object_store " "WHERE name = :name;"_ns, [&self = *this](auto& stmt) -> Result { QM_TRY(MOZ_TO_RESULT(stmt.BindStringByIndex( 0, self.mMetadata.name()))); return Ok{}; }) .map(IsSome), QM_ASSERT_UNREACHABLE); MOZ_ASSERT(!hasResult); } #endif DatabaseConnection::AutoSavepoint autoSave; QM_TRY(MOZ_TO_RESULT(autoSave.Start(Transaction())) #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED , QM_PROPAGATE, MakeAutoSavepointCleanupHandler(*aConnection) #endif ); // The parameter names are not used, parameters are bound by index only // locally in the same function. QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement( "INSERT INTO object_store (id, auto_increment, name, key_path) " "VALUES (:id, :auto_increment, :name, :key_path);"_ns, [&metadata = mMetadata](mozIStorageStatement& stmt) -> Result { QM_TRY(MOZ_TO_RESULT(stmt.BindInt64ByIndex(0, metadata.id()))); QM_TRY(MOZ_TO_RESULT( stmt.BindInt32ByIndex(1, metadata.autoIncrement() ? 1 : 0))); QM_TRY(MOZ_TO_RESULT(stmt.BindStringByIndex(2, metadata.name()))); if (metadata.keyPath().IsValid()) { QM_TRY(MOZ_TO_RESULT(stmt.BindStringByIndex( 3, metadata.keyPath().SerializeToString()))); } else { QM_TRY(MOZ_TO_RESULT(stmt.BindNullByIndex(3))); } return Ok{}; }))); #ifdef DEBUG { int64_t id; MOZ_ALWAYS_SUCCEEDS( aConnection->MutableStorageConnection().GetLastInsertRowID(&id)); MOZ_ASSERT(mMetadata.id() == id); } #endif QM_TRY(MOZ_TO_RESULT(autoSave.Commit())); return NS_OK; } nsresult DeleteObjectStoreOp::DoDatabaseWork(DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); AUTO_PROFILER_LABEL("DeleteObjectStoreOp::DoDatabaseWork", DOM); #ifdef DEBUG { // Make sure |mIsLastObjectStore| is telling the truth. QM_TRY_INSPECT( const auto& stmt, aConnection->BorrowCachedStatement("SELECT id FROM object_store;"_ns), QM_ASSERT_UNREACHABLE); bool foundThisObjectStore = false; bool foundOtherObjectStore = false; while (true) { bool hasResult; MOZ_ALWAYS_SUCCEEDS(stmt->ExecuteStep(&hasResult)); if (!hasResult) { break; } int64_t id; MOZ_ALWAYS_SUCCEEDS(stmt->GetInt64(0, &id)); if (id == mMetadata->mCommonMetadata.id()) { foundThisObjectStore = true; } else { foundOtherObjectStore = true; } } MOZ_ASSERT_IF(mIsLastObjectStore, foundThisObjectStore && !foundOtherObjectStore); MOZ_ASSERT_IF(!mIsLastObjectStore, foundThisObjectStore && foundOtherObjectStore); } #endif DatabaseConnection::AutoSavepoint autoSave; QM_TRY(MOZ_TO_RESULT(autoSave.Start(Transaction())) #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED , QM_PROPAGATE, MakeAutoSavepointCleanupHandler(*aConnection) #endif ); if (mIsLastObjectStore) { // We can just delete everything if this is the last object store. QM_TRY(MOZ_TO_RESULT( aConnection->ExecuteCachedStatement("DELETE FROM index_data;"_ns))); QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement( "DELETE FROM unique_index_data;"_ns))); QM_TRY(MOZ_TO_RESULT( aConnection->ExecuteCachedStatement("DELETE FROM object_data;"_ns))); QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement( "DELETE FROM object_store_index;"_ns))); QM_TRY(MOZ_TO_RESULT( aConnection->ExecuteCachedStatement("DELETE FROM object_store;"_ns))); } else { QM_TRY_INSPECT( const bool& hasIndexes, ObjectStoreHasIndexes(*aConnection, mMetadata->mCommonMetadata.id())); const auto bindObjectStoreIdToFirstParameter = [this](mozIStorageStatement& stmt) -> Result { QM_TRY(MOZ_TO_RESULT( stmt.BindInt64ByIndex(0, mMetadata->mCommonMetadata.id()))); return Ok{}; }; // The parameter name :object_store_id in the SQL statements below is not // used for binding, parameters are bound by index only locally by // bindObjectStoreIdToFirstParameter. if (hasIndexes) { QM_TRY(MOZ_TO_RESULT(DeleteObjectStoreDataTableRowsWithIndexes( aConnection, mMetadata->mCommonMetadata.id(), Nothing()))); // Now clean up the object store index table. QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement( "DELETE FROM object_store_index " "WHERE object_store_id = :object_store_id;"_ns, bindObjectStoreIdToFirstParameter))); } else { // We only have to worry about object data if this object store has no // indexes. QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement( "DELETE FROM object_data " "WHERE object_store_id = :object_store_id;"_ns, bindObjectStoreIdToFirstParameter))); } QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement( "DELETE FROM object_store " "WHERE id = :object_store_id;"_ns, bindObjectStoreIdToFirstParameter))); #ifdef DEBUG { int32_t deletedRowCount; MOZ_ALWAYS_SUCCEEDS( aConnection->MutableStorageConnection().GetAffectedRows( &deletedRowCount)); MOZ_ASSERT(deletedRowCount == 1); } #endif } QM_TRY(MOZ_TO_RESULT(autoSave.Commit())); if (mMetadata->mCommonMetadata.autoIncrement()) { Transaction().ForgetModifiedAutoIncrementObjectStore(*mMetadata); } return NS_OK; } nsresult RenameObjectStoreOp::DoDatabaseWork(DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); AUTO_PROFILER_LABEL("RenameObjectStoreOp::DoDatabaseWork", DOM); #ifdef DEBUG { // Make sure that we're not renaming an object store with the same name as // another that already exists. This should be impossible because we should // have thrown an error long before now... // The parameter names are not used, parameters are bound by index only // locally in the same function. QM_TRY_INSPECT( const bool& hasResult, aConnection ->BorrowAndExecuteSingleStepStatement( "SELECT name " "FROM object_store " "WHERE name = :name AND id != :id;"_ns, [&self = *this](auto& stmt) -> Result { QM_TRY( MOZ_TO_RESULT(stmt.BindStringByIndex(0, self.mNewName))); QM_TRY(MOZ_TO_RESULT(stmt.BindInt64ByIndex(1, self.mId))); return Ok{}; }) .map(IsSome), QM_ASSERT_UNREACHABLE); MOZ_ASSERT(!hasResult); } #endif DatabaseConnection::AutoSavepoint autoSave; QM_TRY(MOZ_TO_RESULT(autoSave.Start(Transaction())) #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED , QM_PROPAGATE, MakeAutoSavepointCleanupHandler(*aConnection) #endif ); // The parameter names are not used, parameters are bound by index only // locally in the same function. QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement( "UPDATE object_store " "SET name = :name " "WHERE id = :id;"_ns, [&self = *this](mozIStorageStatement& stmt) -> Result { QM_TRY(MOZ_TO_RESULT(stmt.BindStringByIndex(0, self.mNewName))); QM_TRY(MOZ_TO_RESULT(stmt.BindInt64ByIndex(1, self.mId))); return Ok{}; }))); QM_TRY(MOZ_TO_RESULT(autoSave.Commit())); return NS_OK; } CreateIndexOp::CreateIndexOp(SafeRefPtr aTransaction, const IndexOrObjectStoreId aObjectStoreId, const IndexMetadata& aMetadata) : VersionChangeTransactionOp(std::move(aTransaction)), mMetadata(aMetadata), mFileManager(Transaction().GetDatabase().GetFileManagerPtr()), mDatabaseId(Transaction().DatabaseId()), mObjectStoreId(aObjectStoreId) { MOZ_ASSERT(aObjectStoreId); MOZ_ASSERT(aMetadata.id()); MOZ_ASSERT(mFileManager); MOZ_ASSERT(!mDatabaseId.IsEmpty()); } nsresult CreateIndexOp::InsertDataFromObjectStore( DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(mMaybeUniqueIndexTable); AUTO_PROFILER_LABEL("CreateIndexOp::InsertDataFromObjectStore", DOM); auto& storageConnection = aConnection->MutableStorageConnection(); RefPtr updateFunction = new UpdateIndexDataValuesFunction(this, aConnection, Transaction().GetDatabasePtr()); constexpr auto updateFunctionName = "update_index_data_values"_ns; nsresult rv = storageConnection.CreateFunction(updateFunctionName, 4, updateFunction); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = InsertDataFromObjectStoreInternal(aConnection); MOZ_ALWAYS_SUCCEEDS(storageConnection.RemoveFunction(updateFunctionName)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } nsresult CreateIndexOp::InsertDataFromObjectStoreInternal( DatabaseConnection* aConnection) const { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(mMaybeUniqueIndexTable); MOZ_ASSERT(aConnection->HasStorageConnection()); // The parameter names are not used, parameters are bound by index only // locally in the same function. QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement( "UPDATE object_data " "SET index_data_values = update_index_data_values " "(key, index_data_values, file_ids, data) " "WHERE object_store_id = :object_store_id;"_ns, [objectStoredId = mObjectStoreId](mozIStorageStatement& stmt) -> Result { QM_TRY(MOZ_TO_RESULT(stmt.BindInt64ByIndex(0, objectStoredId))); return Ok{}; }))); return NS_OK; } bool CreateIndexOp::Init(TransactionBase& aTransaction) { AssertIsOnBackgroundThread(); MOZ_ASSERT(mObjectStoreId); MOZ_ASSERT(mMaybeUniqueIndexTable.isNothing()); const SafeRefPtr objectStoreMetadata = aTransaction.GetMetadataForObjectStoreId(mObjectStoreId); MOZ_ASSERT(objectStoreMetadata); const uint32_t indexCount = objectStoreMetadata->mIndexes.Count(); if (!indexCount) { return true; } auto uniqueIndexTable = UniqueIndexTable{indexCount}; for (const auto& value : objectStoreMetadata->mIndexes.Values()) { MOZ_ASSERT(!uniqueIndexTable.Contains(value->mCommonMetadata.id())); if (NS_WARN_IF(!uniqueIndexTable.InsertOrUpdate( value->mCommonMetadata.id(), value->mCommonMetadata.unique(), fallible))) { IDB_REPORT_INTERNAL_ERR(); NS_WARNING("out of memory"); return false; } } uniqueIndexTable.MarkImmutable(); mMaybeUniqueIndexTable.emplace(std::move(uniqueIndexTable)); return true; } nsresult CreateIndexOp::DoDatabaseWork(DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); AUTO_PROFILER_LABEL("CreateIndexOp::DoDatabaseWork", DOM); #ifdef DEBUG { // Make sure that we're not creating an index with the same name and object // store as another that already exists. This should be impossible because // we should have thrown an error long before now... // The parameter names are not used, parameters are bound by index only // locally in the same function. QM_TRY_INSPECT( const bool& hasResult, aConnection ->BorrowAndExecuteSingleStepStatement( "SELECT name " "FROM object_store_index " "WHERE object_store_id = :object_store_id AND name = :name;"_ns, [&self = *this](auto& stmt) -> Result { QM_TRY(MOZ_TO_RESULT( stmt.BindInt64ByIndex(0, self.mObjectStoreId))); QM_TRY(MOZ_TO_RESULT( stmt.BindStringByIndex(1, self.mMetadata.name()))); return Ok{}; }) .map(IsSome), QM_ASSERT_UNREACHABLE); MOZ_ASSERT(!hasResult); } #endif DatabaseConnection::AutoSavepoint autoSave; QM_TRY(MOZ_TO_RESULT(autoSave.Start(Transaction())) #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED , QM_PROPAGATE, MakeAutoSavepointCleanupHandler(*aConnection) #endif ); // The parameter names are not used, parameters are bound by index only // locally in the same function. QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement( "INSERT INTO object_store_index (id, name, key_path, unique_index, " "multientry, object_store_id, locale, " "is_auto_locale) " "VALUES (:id, :name, :key_path, :unique, :multientry, " ":object_store_id, :locale, :is_auto_locale)"_ns, [&metadata = mMetadata, objectStoreId = mObjectStoreId]( mozIStorageStatement& stmt) -> Result { QM_TRY(MOZ_TO_RESULT(stmt.BindInt64ByIndex(0, metadata.id()))); QM_TRY(MOZ_TO_RESULT(stmt.BindStringByIndex(1, metadata.name()))); QM_TRY(MOZ_TO_RESULT( stmt.BindStringByIndex(2, metadata.keyPath().SerializeToString()))); QM_TRY( MOZ_TO_RESULT(stmt.BindInt32ByIndex(3, metadata.unique() ? 1 : 0))); QM_TRY(MOZ_TO_RESULT( stmt.BindInt32ByIndex(4, metadata.multiEntry() ? 1 : 0))); QM_TRY(MOZ_TO_RESULT(stmt.BindInt64ByIndex(5, objectStoreId))); QM_TRY(MOZ_TO_RESULT( metadata.locale().IsEmpty() ? stmt.BindNullByIndex(6) : stmt.BindUTF8StringByIndex(6, metadata.locale()))); QM_TRY(MOZ_TO_RESULT(stmt.BindInt32ByIndex(7, metadata.autoLocale()))); return Ok{}; }))); #ifdef DEBUG { int64_t id; MOZ_ALWAYS_SUCCEEDS( aConnection->MutableStorageConnection().GetLastInsertRowID(&id)); MOZ_ASSERT(mMetadata.id() == id); } #endif QM_TRY(MOZ_TO_RESULT(InsertDataFromObjectStore(aConnection))); QM_TRY(MOZ_TO_RESULT(autoSave.Commit())); return NS_OK; } NS_IMPL_ISUPPORTS(CreateIndexOp::UpdateIndexDataValuesFunction, mozIStorageFunction); NS_IMETHODIMP CreateIndexOp::UpdateIndexDataValuesFunction::OnFunctionCall( mozIStorageValueArray* aValues, nsIVariant** _retval) { MOZ_ASSERT(aValues); MOZ_ASSERT(_retval); MOZ_ASSERT(mConnection); mConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(mOp); MOZ_ASSERT(mOp->mFileManager); AUTO_PROFILER_LABEL( "CreateIndexOp::UpdateIndexDataValuesFunction::OnFunctionCall", DOM); #ifdef DEBUG { uint32_t argCount; MOZ_ALWAYS_SUCCEEDS(aValues->GetNumEntries(&argCount)); MOZ_ASSERT(argCount == 4); // key, index_data_values, file_ids, data int32_t valueType; MOZ_ALWAYS_SUCCEEDS(aValues->GetTypeOfIndex(0, &valueType)); MOZ_ASSERT(valueType == mozIStorageValueArray::VALUE_TYPE_BLOB); MOZ_ALWAYS_SUCCEEDS(aValues->GetTypeOfIndex(1, &valueType)); MOZ_ASSERT(valueType == mozIStorageValueArray::VALUE_TYPE_NULL || valueType == mozIStorageValueArray::VALUE_TYPE_BLOB); MOZ_ALWAYS_SUCCEEDS(aValues->GetTypeOfIndex(2, &valueType)); MOZ_ASSERT(valueType == mozIStorageValueArray::VALUE_TYPE_NULL || valueType == mozIStorageValueArray::VALUE_TYPE_TEXT); MOZ_ALWAYS_SUCCEEDS(aValues->GetTypeOfIndex(3, &valueType)); MOZ_ASSERT(valueType == mozIStorageValueArray::VALUE_TYPE_BLOB || valueType == mozIStorageValueArray::VALUE_TYPE_INTEGER); } #endif QM_TRY_UNWRAP(auto cloneInfo, GetStructuredCloneReadInfoFromValueArray( aValues, /* aDataIndex */ 3, /* aFileIdsIndex */ 2, *mOp->mFileManager)); const IndexMetadata& metadata = mOp->mMetadata; const IndexOrObjectStoreId& objectStoreId = mOp->mObjectStoreId; // XXX does this really need a non-const cloneInfo? QM_TRY_INSPECT(const auto& updateInfos, DeserializeIndexValueToUpdateInfos( metadata.id(), metadata.keyPath(), metadata.multiEntry(), metadata.locale(), cloneInfo)); if (updateInfos.IsEmpty()) { // XXX See if we can do this without copying... nsCOMPtr unmodifiedValue; // No changes needed, just return the original value. QM_TRY_INSPECT(const int32_t& valueType, MOZ_TO_RESULT_INVOKE_MEMBER(aValues, GetTypeOfIndex, 1)); MOZ_ASSERT(valueType == mozIStorageValueArray::VALUE_TYPE_NULL || valueType == mozIStorageValueArray::VALUE_TYPE_BLOB); if (valueType == mozIStorageValueArray::VALUE_TYPE_NULL) { unmodifiedValue = new storage::NullVariant(); unmodifiedValue.forget(_retval); return NS_OK; } MOZ_ASSERT(valueType == mozIStorageValueArray::VALUE_TYPE_BLOB); const uint8_t* blobData; uint32_t blobDataLength; QM_TRY( MOZ_TO_RESULT(aValues->GetSharedBlob(1, &blobDataLength, &blobData))); const std::pair copiedBlobDataPair( static_cast(malloc(blobDataLength)), blobDataLength); if (!copiedBlobDataPair.first) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_OUT_OF_MEMORY; } memcpy(copiedBlobDataPair.first, blobData, blobDataLength); unmodifiedValue = new storage::AdoptedBlobVariant(copiedBlobDataPair); unmodifiedValue.forget(_retval); return NS_OK; } Key key; QM_TRY(MOZ_TO_RESULT(key.SetFromValueArray(aValues, 0))); QM_TRY_UNWRAP(auto indexValues, ReadCompressedIndexDataValues(*aValues, 1)); const bool hadPreviousIndexValues = !indexValues.IsEmpty(); const uint32_t updateInfoCount = updateInfos.Length(); QM_TRY(OkIf(indexValues.SetCapacity(indexValues.Length() + updateInfoCount, fallible)), NS_ERROR_OUT_OF_MEMORY, IDB_REPORT_INTERNAL_ERR_LAMBDA); // First construct the full list to update the index_data_values row. for (const IndexUpdateInfo& info : updateInfos) { MOZ_ALWAYS_TRUE(indexValues.InsertElementSorted( IndexDataValue(metadata.id(), metadata.unique(), info.value(), info.localizedValue()), fallible)); } QM_TRY_UNWRAP((auto [indexValuesBlob, indexValuesBlobLength]), MakeCompressedIndexDataValues(indexValues)); MOZ_ASSERT(!indexValuesBlobLength == !(indexValuesBlob.get())); nsCOMPtr value; if (!indexValuesBlob) { value = new storage::NullVariant(); value.forget(_retval); return NS_OK; } // Now insert the new table rows. We only need to construct a new list if // the full list is different. if (hadPreviousIndexValues) { indexValues.ClearAndRetainStorage(); MOZ_ASSERT(indexValues.Capacity() >= updateInfoCount); for (const IndexUpdateInfo& info : updateInfos) { MOZ_ALWAYS_TRUE(indexValues.InsertElementSorted( IndexDataValue(metadata.id(), metadata.unique(), info.value(), info.localizedValue()), fallible)); } } QM_TRY(MOZ_TO_RESULT( InsertIndexTableRows(mConnection, objectStoreId, key, indexValues))); value = new storage::AdoptedBlobVariant( std::pair(indexValuesBlob.release(), indexValuesBlobLength)); value.forget(_retval); return NS_OK; } DeleteIndexOp::DeleteIndexOp(SafeRefPtr aTransaction, const IndexOrObjectStoreId aObjectStoreId, const IndexOrObjectStoreId aIndexId, const bool aUnique, const bool aIsLastIndex) : VersionChangeTransactionOp(std::move(aTransaction)), mObjectStoreId(aObjectStoreId), mIndexId(aIndexId), mUnique(aUnique), mIsLastIndex(aIsLastIndex) { MOZ_ASSERT(aObjectStoreId); MOZ_ASSERT(aIndexId); } nsresult DeleteIndexOp::RemoveReferencesToIndex( DatabaseConnection* aConnection, const Key& aObjectStoreKey, nsTArray& aIndexValues) const { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(aConnection); MOZ_ASSERT(!aObjectStoreKey.IsUnset()); MOZ_ASSERT_IF(!mIsLastIndex, !aIndexValues.IsEmpty()); AUTO_PROFILER_LABEL("DeleteIndexOp::RemoveReferencesToIndex", DOM); if (mIsLastIndex) { // There is no need to parse the previous entry in the index_data_values // column if this is the last index. Simply set it to NULL. QM_TRY_INSPECT(const auto& stmt, aConnection->BorrowCachedStatement( "UPDATE object_data " "SET index_data_values = NULL " "WHERE object_store_id = :"_ns + kStmtParamNameObjectStoreId + " AND key = :"_ns + kStmtParamNameKey + ";"_ns)); QM_TRY(MOZ_TO_RESULT( stmt->BindInt64ByName(kStmtParamNameObjectStoreId, mObjectStoreId))); QM_TRY(MOZ_TO_RESULT( aObjectStoreKey.BindToStatement(&*stmt, kStmtParamNameKey))); QM_TRY(MOZ_TO_RESULT(stmt->Execute())); return NS_OK; } { IndexDataValue search; search.mIndexId = mIndexId; // Use raw pointers for search to avoid redundant index validity checks. // Maybe this should better be encapsulated in nsTArray. const auto* const begin = aIndexValues.Elements(); const auto* const end = aIndexValues.Elements() + aIndexValues.Length(); const auto indexIdComparator = [](const IndexDataValue& aA, const IndexDataValue& aB) { return aA.mIndexId < aB.mIndexId; }; MOZ_ASSERT(std::is_sorted(begin, end, indexIdComparator)); const auto [beginRange, endRange] = std::equal_range(begin, end, search, indexIdComparator); if (beginRange == end) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_FILE_CORRUPTED; } aIndexValues.RemoveElementsAt(beginRange - begin, endRange - beginRange); } QM_TRY(MOZ_TO_RESULT(UpdateIndexValues(aConnection, mObjectStoreId, aObjectStoreKey, aIndexValues))); return NS_OK; } nsresult DeleteIndexOp::DoDatabaseWork(DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); #ifdef DEBUG { // Make sure |mIsLastIndex| is telling the truth. // The parameter names are not used, parameters are bound by index only // locally in the same function. QM_TRY_INSPECT(const auto& stmt, aConnection->BorrowCachedStatement( "SELECT id " "FROM object_store_index " "WHERE object_store_id = :object_store_id;"_ns), QM_ASSERT_UNREACHABLE); MOZ_ALWAYS_SUCCEEDS(stmt->BindInt64ByIndex(0, mObjectStoreId)); bool foundThisIndex = false; bool foundOtherIndex = false; while (true) { bool hasResult; MOZ_ALWAYS_SUCCEEDS(stmt->ExecuteStep(&hasResult)); if (!hasResult) { break; } int64_t id; MOZ_ALWAYS_SUCCEEDS(stmt->GetInt64(0, &id)); if (id == mIndexId) { foundThisIndex = true; } else { foundOtherIndex = true; } } MOZ_ASSERT_IF(mIsLastIndex, foundThisIndex && !foundOtherIndex); MOZ_ASSERT_IF(!mIsLastIndex, foundThisIndex && foundOtherIndex); } #endif AUTO_PROFILER_LABEL("DeleteIndexOp::DoDatabaseWork", DOM); DatabaseConnection::AutoSavepoint autoSave; QM_TRY(MOZ_TO_RESULT(autoSave.Start(Transaction())) #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED , QM_PROPAGATE, MakeAutoSavepointCleanupHandler(*aConnection) #endif ); // mozStorage warns that these statements trigger a sort operation but we // don't care because this is a very rare call and we expect it to be slow. // The cost of having an index on this field is too high. QM_TRY_INSPECT( const auto& selectStmt, aConnection->BorrowCachedStatement( mUnique ? (mIsLastIndex ? "/* do not warn (bug someone else) */ " "SELECT value, object_data_key " "FROM unique_index_data " "WHERE index_id = :"_ns + kStmtParamNameIndexId + " ORDER BY object_data_key ASC;"_ns : "/* do not warn (bug out) */ " "SELECT unique_index_data.value, " "unique_index_data.object_data_key, " "object_data.index_data_values " "FROM unique_index_data " "JOIN object_data " "ON unique_index_data.object_data_key = object_data.key " "WHERE unique_index_data.index_id = :"_ns + kStmtParamNameIndexId + " AND object_data.object_store_id = :"_ns + kStmtParamNameObjectStoreId + " ORDER BY unique_index_data.object_data_key ASC;"_ns) : (mIsLastIndex ? "/* do not warn (bug me not) */ " "SELECT value, object_data_key " "FROM index_data " "WHERE index_id = :"_ns + kStmtParamNameIndexId + " AND object_store_id = :"_ns + kStmtParamNameObjectStoreId + " ORDER BY object_data_key ASC;"_ns : "/* do not warn (bug off) */ " "SELECT index_data.value, " "index_data.object_data_key, " "object_data.index_data_values " "FROM index_data " "JOIN object_data " "ON index_data.object_data_key = object_data.key " "WHERE index_data.index_id = :"_ns + kStmtParamNameIndexId + " AND object_data.object_store_id = :"_ns + kStmtParamNameObjectStoreId + " ORDER BY index_data.object_data_key ASC;"_ns))); QM_TRY(MOZ_TO_RESULT( selectStmt->BindInt64ByName(kStmtParamNameIndexId, mIndexId))); if (!mUnique || !mIsLastIndex) { QM_TRY(MOZ_TO_RESULT(selectStmt->BindInt64ByName( kStmtParamNameObjectStoreId, mObjectStoreId))); } Key lastObjectStoreKey; IndexDataValuesAutoArray lastIndexValues; QM_TRY(CollectWhileHasResult( *selectStmt, [this, &aConnection, &lastObjectStoreKey, &lastIndexValues, deleteIndexRowStmt = DatabaseConnection::LazyStatement{ *aConnection, mUnique ? "DELETE FROM unique_index_data " "WHERE index_id = :"_ns + kStmtParamNameIndexId + " AND value = :"_ns + kStmtParamNameValue + ";"_ns : "DELETE FROM index_data " "WHERE index_id = :"_ns + kStmtParamNameIndexId + " AND value = :"_ns + kStmtParamNameValue + " AND object_data_key = :"_ns + kStmtParamNameObjectDataKey + ";"_ns}]( auto& selectStmt) mutable -> Result { // We always need the index key to delete the index row. Key indexKey; QM_TRY(MOZ_TO_RESULT(indexKey.SetFromStatement(&selectStmt, 0))); QM_TRY(OkIf(!indexKey.IsUnset()), Err(NS_ERROR_FILE_CORRUPTED), IDB_REPORT_INTERNAL_ERR_LAMBDA); // Don't call |lastObjectStoreKey.BindToStatement()| directly because we // don't want to copy the same key multiple times. const uint8_t* objectStoreKeyData; uint32_t objectStoreKeyDataLength; QM_TRY(MOZ_TO_RESULT(selectStmt.GetSharedBlob( 1, &objectStoreKeyDataLength, &objectStoreKeyData))); QM_TRY(OkIf(objectStoreKeyDataLength), Err(NS_ERROR_FILE_CORRUPTED), IDB_REPORT_INTERNAL_ERR_LAMBDA); const nsDependentCString currentObjectStoreKeyBuffer( reinterpret_cast(objectStoreKeyData), objectStoreKeyDataLength); if (currentObjectStoreKeyBuffer != lastObjectStoreKey.GetBuffer()) { // We just walked to the next object store key. if (!lastObjectStoreKey.IsUnset()) { // Before we move on to the next key we need to update the previous // key's index_data_values column. QM_TRY(MOZ_TO_RESULT(RemoveReferencesToIndex( aConnection, lastObjectStoreKey, lastIndexValues))); } // Save the object store key. lastObjectStoreKey = Key(currentObjectStoreKeyBuffer); // And the |index_data_values| row if this isn't the only index. if (!mIsLastIndex) { lastIndexValues.ClearAndRetainStorage(); QM_TRY(MOZ_TO_RESULT( ReadCompressedIndexDataValues(selectStmt, 2, lastIndexValues))); QM_TRY(OkIf(!lastIndexValues.IsEmpty()), Err(NS_ERROR_FILE_CORRUPTED), IDB_REPORT_INTERNAL_ERR_LAMBDA); } } // Now delete the index row. { QM_TRY_INSPECT(const auto& borrowedDeleteIndexRowStmt, deleteIndexRowStmt.Borrow()); QM_TRY(MOZ_TO_RESULT(borrowedDeleteIndexRowStmt->BindInt64ByName( kStmtParamNameIndexId, mIndexId))); QM_TRY(MOZ_TO_RESULT(indexKey.BindToStatement( &*borrowedDeleteIndexRowStmt, kStmtParamNameValue))); if (!mUnique) { QM_TRY(MOZ_TO_RESULT(lastObjectStoreKey.BindToStatement( &*borrowedDeleteIndexRowStmt, kStmtParamNameObjectDataKey))); } QM_TRY(MOZ_TO_RESULT(borrowedDeleteIndexRowStmt->Execute())); } return Ok{}; })); // Take care of the last key. if (!lastObjectStoreKey.IsUnset()) { MOZ_ASSERT_IF(!mIsLastIndex, !lastIndexValues.IsEmpty()); QM_TRY(MOZ_TO_RESULT(RemoveReferencesToIndex( aConnection, lastObjectStoreKey, lastIndexValues))); } QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement( "DELETE FROM object_store_index " "WHERE id = :index_id;"_ns, [indexId = mIndexId](mozIStorageStatement& deleteStmt) -> Result { QM_TRY(MOZ_TO_RESULT(deleteStmt.BindInt64ByIndex(0, indexId))); return Ok{}; }))); #ifdef DEBUG { int32_t deletedRowCount; MOZ_ALWAYS_SUCCEEDS(aConnection->MutableStorageConnection().GetAffectedRows( &deletedRowCount)); MOZ_ASSERT(deletedRowCount == 1); } #endif QM_TRY(MOZ_TO_RESULT(autoSave.Commit())); return NS_OK; } nsresult RenameIndexOp::DoDatabaseWork(DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); AUTO_PROFILER_LABEL("RenameIndexOp::DoDatabaseWork", DOM); #ifdef DEBUG { // Make sure that we're not renaming an index with the same name as another // that already exists. This should be impossible because we should have // thrown an error long before now... // The parameter names are not used, parameters are bound by index only // locally in the same function. QM_TRY_INSPECT(const bool& hasResult, aConnection ->BorrowAndExecuteSingleStepStatement( "SELECT name " "FROM object_store_index " "WHERE object_store_id = :object_store_id " "AND name = :name " "AND id != :id;"_ns, [&self = *this](auto& stmt) -> Result { QM_TRY(MOZ_TO_RESULT(stmt.BindInt64ByIndex( 0, self.mObjectStoreId))); QM_TRY(MOZ_TO_RESULT( stmt.BindStringByIndex(1, self.mNewName))); QM_TRY(MOZ_TO_RESULT( stmt.BindInt64ByIndex(2, self.mIndexId))); return Ok{}; }) .map(IsSome), QM_ASSERT_UNREACHABLE); MOZ_ASSERT(!hasResult); } #else Unused << mObjectStoreId; #endif DatabaseConnection::AutoSavepoint autoSave; QM_TRY(MOZ_TO_RESULT(autoSave.Start(Transaction())) #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED , QM_PROPAGATE, MakeAutoSavepointCleanupHandler(*aConnection) #endif ); // The parameter names are not used, parameters are bound by index only // locally in the same function. QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement( "UPDATE object_store_index " "SET name = :name " "WHERE id = :id;"_ns, [&self = *this](mozIStorageStatement& stmt) -> Result { QM_TRY(MOZ_TO_RESULT(stmt.BindStringByIndex(0, self.mNewName))); QM_TRY(MOZ_TO_RESULT(stmt.BindInt64ByIndex(1, self.mIndexId))); return Ok{}; }))); QM_TRY(MOZ_TO_RESULT(autoSave.Commit())); return NS_OK; } Result NormalTransactionOp::ObjectStoreHasIndexes( DatabaseConnection& aConnection, const IndexOrObjectStoreId aObjectStoreId, const bool aMayHaveIndexes) { aConnection.AssertIsOnConnectionThread(); MOZ_ASSERT(aObjectStoreId); if (Transaction().GetMode() == IDBTransaction::Mode::VersionChange && aMayHaveIndexes) { // If this is a version change transaction then mObjectStoreMayHaveIndexes // could be wrong (e.g. if a unique index failed to be created due to a // constraint error). We have to check on this thread by asking the database // directly. QM_TRY_RETURN(DatabaseOperationBase::ObjectStoreHasIndexes(aConnection, aObjectStoreId)); } #ifdef DEBUG QM_TRY_INSPECT( const bool& hasIndexes, DatabaseOperationBase::ObjectStoreHasIndexes(aConnection, aObjectStoreId), QM_ASSERT_UNREACHABLE); MOZ_ASSERT(aMayHaveIndexes == hasIndexes); #endif return aMayHaveIndexes; } Result NormalTransactionOp::GetPreprocessParams() { return PreprocessParams{}; } nsresult NormalTransactionOp::SendPreprocessInfo() { AssertIsOnOwningThread(); MOZ_ASSERT(!IsActorDestroyed()); QM_TRY_INSPECT(const auto& params, GetPreprocessParams()); MOZ_ASSERT(params.type() != PreprocessParams::T__None); if (NS_WARN_IF(!PBackgroundIDBRequestParent::SendPreprocess(params))) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } return NS_OK; } nsresult NormalTransactionOp::SendSuccessResult() { AssertIsOnOwningThread(); if (!IsActorDestroyed()) { static const size_t kMaxIDBMsgOverhead = 1024 * 1024 * 10; // 10MB const uint32_t maximalSizeFromPref = IndexedDatabaseManager::MaxSerializedMsgSize(); MOZ_ASSERT(maximalSizeFromPref > kMaxIDBMsgOverhead); const size_t kMaxMessageSize = maximalSizeFromPref - kMaxIDBMsgOverhead; RequestResponse response; size_t responseSize = kMaxMessageSize; GetResponse(response, &responseSize); if (responseSize >= kMaxMessageSize) { nsPrintfCString warning( "The serialized value is too large" " (size=%zu bytes, max=%zu bytes).", responseSize, kMaxMessageSize); NS_WARNING(warning.get()); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } MOZ_ASSERT(response.type() != RequestResponse::T__None); if (response.type() == RequestResponse::Tnsresult) { MOZ_ASSERT(NS_FAILED(response.get_nsresult())); return response.get_nsresult(); } if (NS_WARN_IF( !PBackgroundIDBRequestParent::Send__delete__(this, response))) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } } #ifdef DEBUG mResponseSent = true; #endif return NS_OK; } bool NormalTransactionOp::SendFailureResult(nsresult aResultCode) { AssertIsOnOwningThread(); MOZ_ASSERT(NS_FAILED(aResultCode)); bool result = false; if (!IsActorDestroyed()) { result = PBackgroundIDBRequestParent::Send__delete__( this, ClampResultCode(aResultCode)); } #ifdef DEBUG mResponseSent = true; #endif return result; } void NormalTransactionOp::Cleanup() { AssertIsOnOwningThread(); MOZ_ASSERT_IF(!IsActorDestroyed(), mResponseSent); TransactionDatabaseOperationBase::Cleanup(); } void NormalTransactionOp::ActorDestroy(ActorDestroyReason aWhy) { AssertIsOnOwningThread(); NoteActorDestroyed(); // Assume ActorDestroy can happen at any time, so we can't probe the current // state since mInternalState can be modified on any thread (only one thread // at a time based on the state machine). // However we can use mWaitingForContinue which is only touched on the owning // thread. If mWaitingForContinue is true, we can also modify mInternalState // since we are guaranteed that there are no pending runnables which would // probe mInternalState to decide what code needs to run (there shouldn't be // any running runnables on other threads either). if (IsWaitingForContinue()) { NoteContinueReceived(); } // We don't have to handle the case when mWaitingForContinue is not true since // it means that either nothing has been initialized yet, so nothing to // cleanup or there are pending runnables that will detect that the actor has // been destroyed and cleanup accordingly. } mozilla::ipc::IPCResult NormalTransactionOp::RecvContinue( const PreprocessResponse& aResponse) { AssertIsOnOwningThread(); switch (aResponse.type()) { case PreprocessResponse::Tnsresult: SetFailureCode(aResponse.get_nsresult()); break; case PreprocessResponse::TObjectStoreGetPreprocessResponse: case PreprocessResponse::TObjectStoreGetAllPreprocessResponse: break; default: MOZ_CRASH("Should never get here!"); } NoteContinueReceived(); return IPC_OK(); } ObjectStoreAddOrPutRequestOp::ObjectStoreAddOrPutRequestOp( SafeRefPtr aTransaction, RequestParams&& aParams) : NormalTransactionOp(std::move(aTransaction)), mParams( std::move(aParams.type() == RequestParams::TObjectStoreAddParams ? aParams.get_ObjectStoreAddParams().commonParams() : aParams.get_ObjectStorePutParams().commonParams())), mOriginMetadata(Transaction().GetDatabase().OriginMetadata()), mPersistenceType(Transaction().GetDatabase().Type()), mOverwrite(aParams.type() == RequestParams::TObjectStorePutParams), mObjectStoreMayHaveIndexes(false) { MOZ_ASSERT(aParams.type() == RequestParams::TObjectStoreAddParams || aParams.type() == RequestParams::TObjectStorePutParams); mMetadata = Transaction().GetMetadataForObjectStoreId(mParams.objectStoreId()); MOZ_ASSERT(mMetadata); mObjectStoreMayHaveIndexes = mMetadata->HasLiveIndexes(); mDataOverThreshold = snappy::MaxCompressedLength(mParams.cloneInfo().data().data.Size()) > IndexedDatabaseManager::DataThreshold(); } nsresult ObjectStoreAddOrPutRequestOp::RemoveOldIndexDataValues( DatabaseConnection* aConnection) { AssertIsOnConnectionThread(); MOZ_ASSERT(aConnection); MOZ_ASSERT(mOverwrite); MOZ_ASSERT(!mResponse.IsUnset()); #ifdef DEBUG { QM_TRY_INSPECT(const bool& hasIndexes, DatabaseOperationBase::ObjectStoreHasIndexes( *aConnection, mParams.objectStoreId()), QM_ASSERT_UNREACHABLE); MOZ_ASSERT(hasIndexes, "Don't use this slow method if there are no indexes!"); } #endif QM_TRY_INSPECT( const auto& indexValuesStmt, aConnection->BorrowAndExecuteSingleStepStatement( "SELECT index_data_values " "FROM object_data " "WHERE object_store_id = :"_ns + kStmtParamNameObjectStoreId + " AND key = :"_ns + kStmtParamNameKey + ";"_ns, [&self = *this](auto& stmt) -> mozilla::Result { QM_TRY(MOZ_TO_RESULT(stmt.BindInt64ByName( kStmtParamNameObjectStoreId, self.mParams.objectStoreId()))); QM_TRY(MOZ_TO_RESULT( self.mResponse.BindToStatement(&stmt, kStmtParamNameKey))); return Ok{}; })); if (indexValuesStmt) { QM_TRY_INSPECT(const auto& existingIndexValues, ReadCompressedIndexDataValues(**indexValuesStmt, 0)); QM_TRY(MOZ_TO_RESULT( DeleteIndexDataTableRows(aConnection, mResponse, existingIndexValues))); } return NS_OK; } bool ObjectStoreAddOrPutRequestOp::Init(TransactionBase& aTransaction) { AssertIsOnOwningThread(); const nsTArray& indexUpdateInfos = mParams.indexUpdateInfos(); if (!indexUpdateInfos.IsEmpty()) { mUniqueIndexTable.emplace(); for (const auto& updateInfo : indexUpdateInfos) { auto indexMetadata = mMetadata->mIndexes.Lookup(updateInfo.indexId()); MOZ_ALWAYS_TRUE(indexMetadata); MOZ_ASSERT(!(*indexMetadata)->mDeleted); const IndexOrObjectStoreId& indexId = (*indexMetadata)->mCommonMetadata.id(); const bool& unique = (*indexMetadata)->mCommonMetadata.unique(); MOZ_ASSERT(indexId == updateInfo.indexId()); MOZ_ASSERT_IF(!(*indexMetadata)->mCommonMetadata.multiEntry(), !mUniqueIndexTable.ref().Contains(indexId)); if (NS_WARN_IF(!mUniqueIndexTable.ref().InsertOrUpdate(indexId, unique, fallible))) { return false; } } } else if (mOverwrite) { mUniqueIndexTable.emplace(); } if (mUniqueIndexTable.isSome()) { mUniqueIndexTable.ref().MarkImmutable(); } QM_TRY_UNWRAP( mStoredFileInfos, TransformIntoNewArray( mParams.fileAddInfos(), [](const auto& fileAddInfo) { MOZ_ASSERT(fileAddInfo.type() == StructuredCloneFileBase::eBlob || fileAddInfo.type() == StructuredCloneFileBase::eMutableFile); switch (fileAddInfo.type()) { case StructuredCloneFileBase::eBlob: { PBackgroundIDBDatabaseFileParent* file = fileAddInfo.file().AsParent(); MOZ_ASSERT(file); auto* const fileActor = static_cast(file); MOZ_ASSERT(fileActor); return StoredFileInfo::CreateForBlob( fileActor->GetFileInfoPtr(), fileActor); } default: MOZ_CRASH("Should never get here!"); } }, fallible), false); if (mDataOverThreshold) { auto fileInfo = aTransaction.GetDatabase().GetFileManager().CreateFileInfo(); if (NS_WARN_IF(!fileInfo)) { return false; } mStoredFileInfos.EmplaceBack(StoredFileInfo::CreateForStructuredClone( std::move(fileInfo), MakeRefPtr(mParams.cloneInfo().data().data))); } return true; } nsresult ObjectStoreAddOrPutRequestOp::DoDatabaseWork( DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(aConnection->HasStorageConnection()); AUTO_PROFILER_LABEL("ObjectStoreAddOrPutRequestOp::DoDatabaseWork", DOM); DatabaseConnection::AutoSavepoint autoSave; QM_TRY(MOZ_TO_RESULT(autoSave.Start(Transaction())) #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED , QM_PROPAGATE, MakeAutoSavepointCleanupHandler(*aConnection) #endif ); QM_TRY_INSPECT(const bool& objectStoreHasIndexes, ObjectStoreHasIndexes(*aConnection, mParams.objectStoreId(), mObjectStoreMayHaveIndexes)); // This will be the final key we use. Key& key = mResponse; key = mParams.key(); const bool keyUnset = key.IsUnset(); const IndexOrObjectStoreId osid = mParams.objectStoreId(); // First delete old index_data_values if we're overwriting something and we // have indexes. if (mOverwrite && !keyUnset && objectStoreHasIndexes) { QM_TRY(MOZ_TO_RESULT(RemoveOldIndexDataValues(aConnection))); } int64_t autoIncrementNum = 0; { // The "|| keyUnset" here is mostly a debugging tool. If a key isn't // specified we should never have a collision and so it shouldn't matter // if we allow overwrite or not. By not allowing overwrite we raise // detectable errors rather than corrupting data. const auto optReplaceDirective = (!mOverwrite || keyUnset) ? ""_ns : "OR REPLACE "_ns; QM_TRY_INSPECT(const auto& stmt, aConnection->BorrowCachedStatement( "INSERT "_ns + optReplaceDirective + "INTO object_data " "(object_store_id, key, file_ids, data) " "VALUES (:"_ns + kStmtParamNameObjectStoreId + ", :"_ns + kStmtParamNameKey + ", :"_ns + kStmtParamNameFileIds + ", :"_ns + kStmtParamNameData + ");"_ns)); QM_TRY(MOZ_TO_RESULT( stmt->BindInt64ByName(kStmtParamNameObjectStoreId, osid))); const SerializedStructuredCloneWriteInfo& cloneInfo = mParams.cloneInfo(); const JSStructuredCloneData& cloneData = cloneInfo.data().data; const size_t cloneDataSize = cloneData.Size(); MOZ_ASSERT(!keyUnset || mMetadata->mCommonMetadata.autoIncrement(), "Should have key unless autoIncrement"); if (mMetadata->mCommonMetadata.autoIncrement()) { if (keyUnset) { { const auto&& lockedAutoIncrementIds = mMetadata->mAutoIncrementIds.Lock(); autoIncrementNum = lockedAutoIncrementIds->next; } MOZ_ASSERT(autoIncrementNum > 0); if (autoIncrementNum > (1LL << 53)) { return NS_ERROR_DOM_INDEXEDDB_CONSTRAINT_ERR; } QM_TRY(key.SetFromInteger(autoIncrementNum)); } else if (key.IsFloat()) { double numericKey = key.ToFloat(); numericKey = std::min(numericKey, double(1LL << 53)); numericKey = floor(numericKey); const auto&& lockedAutoIncrementIds = mMetadata->mAutoIncrementIds.Lock(); if (numericKey >= lockedAutoIncrementIds->next) { autoIncrementNum = numericKey; } } if (keyUnset && mMetadata->mCommonMetadata.keyPath().IsValid()) { const SerializedStructuredCloneWriteInfo& cloneInfo = mParams.cloneInfo(); MOZ_ASSERT(cloneInfo.offsetToKeyProp()); MOZ_ASSERT(cloneDataSize > sizeof(uint64_t)); MOZ_ASSERT(cloneInfo.offsetToKeyProp() <= (cloneDataSize - sizeof(uint64_t))); // Special case where someone put an object into an autoIncrement'ing // objectStore with no key in its keyPath set. We needed to figure out // which row id we would get above before we could set that properly. uint64_t keyPropValue = ReinterpretDoubleAsUInt64(static_cast(autoIncrementNum)); static const size_t keyPropSize = sizeof(uint64_t); char keyPropBuffer[keyPropSize]; LittleEndian::writeUint64(keyPropBuffer, keyPropValue); auto iter = cloneData.Start(); MOZ_ALWAYS_TRUE(cloneData.Advance(iter, cloneInfo.offsetToKeyProp())); MOZ_ALWAYS_TRUE( cloneData.UpdateBytes(iter, keyPropBuffer, keyPropSize)); } } key.BindToStatement(&*stmt, kStmtParamNameKey); if (mDataOverThreshold) { // The data we store in the SQLite database is a (signed) 64-bit integer. // The flags are left-shifted 32 bits so the max value is 0xFFFFFFFF. // The file_ids index occupies the lower 32 bits and its max is // 0xFFFFFFFF. static const uint32_t kCompressedFlag = (1 << 0); uint32_t flags = 0; flags |= kCompressedFlag; const uint32_t index = mStoredFileInfos.Length() - 1; const int64_t data = (uint64_t(flags) << 32) | index; QM_TRY(MOZ_TO_RESULT(stmt->BindInt64ByName(kStmtParamNameData, data))); } else { AutoTArray flatCloneData; // 4096 from JSStructuredCloneData QM_TRY(OkIf(flatCloneData.SetLength(cloneDataSize, fallible)), Err(NS_ERROR_OUT_OF_MEMORY)); { auto iter = cloneData.Start(); MOZ_ALWAYS_TRUE( cloneData.ReadBytes(iter, flatCloneData.Elements(), cloneDataSize)); } // Compress the bytes before adding into the database. const char* const uncompressed = flatCloneData.Elements(); const size_t uncompressedLength = cloneDataSize; size_t compressedLength = snappy::MaxCompressedLength(uncompressedLength); UniqueFreePtr compressed( static_cast(malloc(compressedLength))); if (NS_WARN_IF(!compressed)) { return NS_ERROR_OUT_OF_MEMORY; } snappy::RawCompress(uncompressed, uncompressedLength, compressed.get(), &compressedLength); uint8_t* const dataBuffer = reinterpret_cast(compressed.release()); const size_t dataBufferLength = compressedLength; QM_TRY(MOZ_TO_RESULT(stmt->BindAdoptedBlobByName( kStmtParamNameData, dataBuffer, dataBufferLength))); } if (!mStoredFileInfos.IsEmpty()) { // Moved outside the loop to allow it to be cached when demanded by the // first write. (We may have mStoredFileInfos without any required // writes.) Maybe fileHelper; nsAutoString fileIds; for (auto& storedFileInfo : mStoredFileInfos) { MOZ_ASSERT(storedFileInfo.IsValid()); QM_TRY_INSPECT(const auto& inputStream, storedFileInfo.GetInputStream()); if (inputStream) { if (fileHelper.isNothing()) { fileHelper.emplace(Transaction().GetDatabase().GetFileManagerPtr()); QM_TRY(MOZ_TO_RESULT(fileHelper->Init()), NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR, IDB_REPORT_INTERNAL_ERR_LAMBDA); } const DatabaseFileInfo& fileInfo = storedFileInfo.GetFileInfo(); const DatabaseFileManager& fileManager = fileInfo.Manager(); const auto file = fileHelper->GetFile(fileInfo); QM_TRY(OkIf(file), NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR, IDB_REPORT_INTERNAL_ERR_LAMBDA); const auto journalFile = fileHelper->GetJournalFile(fileInfo); QM_TRY(OkIf(journalFile), NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR, IDB_REPORT_INTERNAL_ERR_LAMBDA); nsCString fileKeyId; fileKeyId.AppendInt(fileInfo.Id()); const auto maybeKey = fileManager.IsInPrivateBrowsingMode() ? fileManager.MutableCipherKeyManagerRef().Get(fileKeyId) : Nothing(); QM_TRY(MOZ_TO_RESULT(fileHelper->CreateFileFromStream( *file, *journalFile, *inputStream, storedFileInfo.ShouldCompress(), maybeKey)) .mapErr([](const nsresult rv) { if (NS_ERROR_GET_MODULE(rv) != NS_ERROR_MODULE_DOM_INDEXEDDB) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } return rv; }), QM_PROPAGATE, ([&fileManager, &file = *file, &journalFile = *journalFile](const auto) { // Try to remove the file if the copy failed. QM_TRY(MOZ_TO_RESULT( fileManager.SyncDeleteFile(file, journalFile)), QM_VOID); })); storedFileInfo.NotifyWriteSucceeded(); } if (!fileIds.IsEmpty()) { fileIds.Append(' '); } storedFileInfo.Serialize(fileIds); } QM_TRY(MOZ_TO_RESULT( stmt->BindStringByName(kStmtParamNameFileIds, fileIds))); } else { QM_TRY(MOZ_TO_RESULT(stmt->BindNullByName(kStmtParamNameFileIds))); } QM_TRY(MOZ_TO_RESULT(stmt->Execute()), QM_PROPAGATE, [keyUnset = DebugOnly{keyUnset}](const nsresult rv) { if (rv == NS_ERROR_STORAGE_CONSTRAINT) { MOZ_ASSERT(!keyUnset, "Generated key had a collision!"); } }); } // Update our indexes if needed. if (!mParams.indexUpdateInfos().IsEmpty()) { MOZ_ASSERT(mUniqueIndexTable.isSome()); // Write the index_data_values column. QM_TRY_INSPECT(const auto& indexValues, IndexDataValuesFromUpdateInfos(mParams.indexUpdateInfos(), mUniqueIndexTable.ref())); QM_TRY( MOZ_TO_RESULT(UpdateIndexValues(aConnection, osid, key, indexValues))); QM_TRY(MOZ_TO_RESULT( InsertIndexTableRows(aConnection, osid, key, indexValues))); } QM_TRY(MOZ_TO_RESULT(autoSave.Commit())); if (autoIncrementNum) { { auto&& lockedAutoIncrementIds = mMetadata->mAutoIncrementIds.Lock(); lockedAutoIncrementIds->next = autoIncrementNum + 1; } Transaction().NoteModifiedAutoIncrementObjectStore(mMetadata); } return NS_OK; } void ObjectStoreAddOrPutRequestOp::GetResponse(RequestResponse& aResponse, size_t* aResponseSize) { AssertIsOnOwningThread(); if (mOverwrite) { aResponse = ObjectStorePutResponse(mResponse); *aResponseSize = mResponse.GetBuffer().Length(); } else { aResponse = ObjectStoreAddResponse(mResponse); *aResponseSize = mResponse.GetBuffer().Length(); } } void ObjectStoreAddOrPutRequestOp::Cleanup() { AssertIsOnOwningThread(); mStoredFileInfos.Clear(); NormalTransactionOp::Cleanup(); } NS_IMPL_ISUPPORTS(ObjectStoreAddOrPutRequestOp::SCInputStream, nsIInputStream) NS_IMETHODIMP ObjectStoreAddOrPutRequestOp::SCInputStream::Close() { return NS_OK; } NS_IMETHODIMP ObjectStoreAddOrPutRequestOp::SCInputStream::Available(uint64_t* _retval) { return NS_ERROR_NOT_IMPLEMENTED; } NS_IMETHODIMP ObjectStoreAddOrPutRequestOp::SCInputStream::StreamStatus() { return NS_OK; } NS_IMETHODIMP ObjectStoreAddOrPutRequestOp::SCInputStream::Read(char* aBuf, uint32_t aCount, uint32_t* _retval) { return ReadSegments(NS_CopySegmentToBuffer, aBuf, aCount, _retval); } NS_IMETHODIMP ObjectStoreAddOrPutRequestOp::SCInputStream::ReadSegments( nsWriteSegmentFun aWriter, void* aClosure, uint32_t aCount, uint32_t* _retval) { *_retval = 0; while (aCount) { uint32_t count = std::min(uint32_t(mIter.RemainingInSegment()), aCount); if (!count) { // We've run out of data in the last segment. break; } uint32_t written; nsresult rv = aWriter(this, aClosure, mIter.Data(), *_retval, count, &written); if (NS_WARN_IF(NS_FAILED(rv))) { // InputStreams do not propagate errors to caller. return NS_OK; } // Writer should write what we asked it to write. MOZ_ASSERT(written == count); *_retval += count; aCount -= count; if (NS_WARN_IF(!mData.Advance(mIter, count))) { // InputStreams do not propagate errors to caller. return NS_OK; } } return NS_OK; } NS_IMETHODIMP ObjectStoreAddOrPutRequestOp::SCInputStream::IsNonBlocking(bool* _retval) { *_retval = false; return NS_OK; } ObjectStoreGetRequestOp::ObjectStoreGetRequestOp( SafeRefPtr aTransaction, const RequestParams& aParams, bool aGetAll) : NormalTransactionOp(std::move(aTransaction)), mObjectStoreId(aGetAll ? aParams.get_ObjectStoreGetAllParams().objectStoreId() : aParams.get_ObjectStoreGetParams().objectStoreId()), mDatabase(Transaction().GetDatabasePtr()), mOptionalKeyRange( aGetAll ? aParams.get_ObjectStoreGetAllParams().optionalKeyRange() : Some(aParams.get_ObjectStoreGetParams().keyRange())), mBackgroundParent(Transaction().GetBackgroundParent()), mPreprocessInfoCount(0), mLimit(aGetAll ? aParams.get_ObjectStoreGetAllParams().limit() : 1), mGetAll(aGetAll) { MOZ_ASSERT(aParams.type() == RequestParams::TObjectStoreGetParams || aParams.type() == RequestParams::TObjectStoreGetAllParams); MOZ_ASSERT(mObjectStoreId); MOZ_ASSERT(mDatabase); MOZ_ASSERT_IF(!aGetAll, mOptionalKeyRange.isSome()); MOZ_ASSERT(mBackgroundParent); } template Result ObjectStoreGetRequestOp::ConvertResponse( StructuredCloneReadInfoParent&& aInfo) { T result; static_assert(std::is_same_v || std::is_same_v); if constexpr (std::is_same_v) { result.data().data = aInfo.ReleaseData(); result.hasPreprocessInfo() = aInfo.HasPreprocessInfo(); } QM_TRY_UNWRAP(result.files(), SerializeStructuredCloneFiles( mDatabase, aInfo.Files(), std::is_same_v)); return result; } nsresult ObjectStoreGetRequestOp::DoDatabaseWork( DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); MOZ_ASSERT_IF(!mGetAll, mOptionalKeyRange.isSome()); MOZ_ASSERT_IF(!mGetAll, mLimit == 1); AUTO_PROFILER_LABEL("ObjectStoreGetRequestOp::DoDatabaseWork", DOM); const nsCString query = "SELECT file_ids, data " "FROM object_data " "WHERE object_store_id = :"_ns + kStmtParamNameObjectStoreId + MaybeGetBindingClauseForKeyRange(mOptionalKeyRange, kColumnNameKey) + " ORDER BY key ASC"_ns + (mLimit ? kOpenLimit + IntToCString(mLimit) : EmptyCString()); QM_TRY_INSPECT(const auto& stmt, aConnection->BorrowCachedStatement(query)); QM_TRY(MOZ_TO_RESULT( stmt->BindInt64ByName(kStmtParamNameObjectStoreId, mObjectStoreId))); if (mOptionalKeyRange.isSome()) { QM_TRY(MOZ_TO_RESULT( BindKeyRangeToStatement(mOptionalKeyRange.ref(), &*stmt))); } QM_TRY(CollectWhileHasResult( *stmt, [this](auto& stmt) mutable -> mozilla::Result { QM_TRY_UNWRAP(auto cloneInfo, GetStructuredCloneReadInfoFromStatement( &stmt, 1, 0, mDatabase->GetFileManager())); if (cloneInfo.HasPreprocessInfo()) { mPreprocessInfoCount++; } QM_TRY(OkIf(mResponse.EmplaceBack(fallible, std::move(cloneInfo))), Err(NS_ERROR_OUT_OF_MEMORY)); return Ok{}; })); MOZ_ASSERT_IF(!mGetAll, mResponse.Length() <= 1); return NS_OK; } bool ObjectStoreGetRequestOp::HasPreprocessInfo() { return mPreprocessInfoCount > 0; } Result ObjectStoreGetRequestOp::GetPreprocessParams() { AssertIsOnOwningThread(); MOZ_ASSERT(!mResponse.IsEmpty()); if (mGetAll) { auto params = ObjectStoreGetAllPreprocessParams(); auto& preprocessInfos = params.preprocessInfos(); if (NS_WARN_IF( !preprocessInfos.SetCapacity(mPreprocessInfoCount, fallible))) { return Err(NS_ERROR_OUT_OF_MEMORY); } QM_TRY(TransformIfAbortOnErr( std::make_move_iterator(mResponse.begin()), std::make_move_iterator(mResponse.end()), MakeBackInserter(preprocessInfos), [](const auto& info) { return info.HasPreprocessInfo(); }, [&self = *this](StructuredCloneReadInfoParent&& info) { return self.ConvertResponse(std::move(info)); })); return PreprocessParams{std::move(params)}; } auto params = ObjectStoreGetPreprocessParams(); QM_TRY_UNWRAP(params.preprocessInfo(), ConvertResponse(std::move(mResponse[0]))); return PreprocessParams{std::move(params)}; } void ObjectStoreGetRequestOp::GetResponse(RequestResponse& aResponse, size_t* aResponseSize) { MOZ_ASSERT_IF(mLimit, mResponse.Length() <= mLimit); if (mGetAll) { aResponse = ObjectStoreGetAllResponse(); *aResponseSize = 0; if (!mResponse.IsEmpty()) { QM_TRY_UNWRAP( aResponse.get_ObjectStoreGetAllResponse().cloneInfos(), TransformIntoNewArrayAbortOnErr( std::make_move_iterator(mResponse.begin()), std::make_move_iterator(mResponse.end()), [this, &aResponseSize](StructuredCloneReadInfoParent&& info) { *aResponseSize += info.Size(); return ConvertResponse( std::move(info)); }, fallible), QM_VOID, [&aResponse](const nsresult result) { aResponse = result; }); } return; } aResponse = ObjectStoreGetResponse(); *aResponseSize = 0; if (!mResponse.IsEmpty()) { SerializedStructuredCloneReadInfo& serializedInfo = aResponse.get_ObjectStoreGetResponse().cloneInfo(); *aResponseSize += mResponse[0].Size(); QM_TRY_UNWRAP(serializedInfo, ConvertResponse( std::move(mResponse[0])), QM_VOID, [&aResponse](const nsresult result) { aResponse = result; }); } } ObjectStoreGetKeyRequestOp::ObjectStoreGetKeyRequestOp( SafeRefPtr aTransaction, const RequestParams& aParams, bool aGetAll) : NormalTransactionOp(std::move(aTransaction)), mObjectStoreId( aGetAll ? aParams.get_ObjectStoreGetAllKeysParams().objectStoreId() : aParams.get_ObjectStoreGetKeyParams().objectStoreId()), mOptionalKeyRange( aGetAll ? aParams.get_ObjectStoreGetAllKeysParams().optionalKeyRange() : Some(aParams.get_ObjectStoreGetKeyParams().keyRange())), mLimit(aGetAll ? aParams.get_ObjectStoreGetAllKeysParams().limit() : 1), mGetAll(aGetAll) { MOZ_ASSERT(aParams.type() == RequestParams::TObjectStoreGetKeyParams || aParams.type() == RequestParams::TObjectStoreGetAllKeysParams); MOZ_ASSERT(mObjectStoreId); MOZ_ASSERT_IF(!aGetAll, mOptionalKeyRange.isSome()); } nsresult ObjectStoreGetKeyRequestOp::DoDatabaseWork( DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); AUTO_PROFILER_LABEL("ObjectStoreGetKeyRequestOp::DoDatabaseWork", DOM); const nsCString query = "SELECT key " "FROM object_data " "WHERE object_store_id = :"_ns + kStmtParamNameObjectStoreId + MaybeGetBindingClauseForKeyRange(mOptionalKeyRange, kColumnNameKey) + " ORDER BY key ASC"_ns + (mLimit ? " LIMIT "_ns + IntToCString(mLimit) : EmptyCString()); QM_TRY_INSPECT(const auto& stmt, aConnection->BorrowCachedStatement(query)); nsresult rv = stmt->BindInt64ByName(kStmtParamNameObjectStoreId, mObjectStoreId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (mOptionalKeyRange.isSome()) { rv = BindKeyRangeToStatement(mOptionalKeyRange.ref(), &*stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } QM_TRY(CollectWhileHasResult( *stmt, [this](auto& stmt) mutable -> mozilla::Result { Key* const key = mResponse.AppendElement(fallible); QM_TRY(OkIf(key), Err(NS_ERROR_OUT_OF_MEMORY)); QM_TRY(MOZ_TO_RESULT(key->SetFromStatement(&stmt, 0))); return Ok{}; })); MOZ_ASSERT_IF(!mGetAll, mResponse.Length() <= 1); return NS_OK; } void ObjectStoreGetKeyRequestOp::GetResponse(RequestResponse& aResponse, size_t* aResponseSize) { MOZ_ASSERT_IF(mLimit, mResponse.Length() <= mLimit); if (mGetAll) { aResponse = ObjectStoreGetAllKeysResponse(); *aResponseSize = std::accumulate(mResponse.begin(), mResponse.end(), 0u, [](size_t old, const auto& entry) { return old + entry.GetBuffer().Length(); }); aResponse.get_ObjectStoreGetAllKeysResponse().keys() = std::move(mResponse); return; } aResponse = ObjectStoreGetKeyResponse(); *aResponseSize = 0; if (!mResponse.IsEmpty()) { *aResponseSize = mResponse[0].GetBuffer().Length(); aResponse.get_ObjectStoreGetKeyResponse().key() = std::move(mResponse[0]); } } ObjectStoreDeleteRequestOp::ObjectStoreDeleteRequestOp( SafeRefPtr aTransaction, const ObjectStoreDeleteParams& aParams) : NormalTransactionOp(std::move(aTransaction)), mParams(aParams), mObjectStoreMayHaveIndexes(false) { AssertIsOnBackgroundThread(); SafeRefPtr metadata = Transaction().GetMetadataForObjectStoreId(mParams.objectStoreId()); MOZ_ASSERT(metadata); mObjectStoreMayHaveIndexes = metadata->HasLiveIndexes(); } nsresult ObjectStoreDeleteRequestOp::DoDatabaseWork( DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); AUTO_PROFILER_LABEL("ObjectStoreDeleteRequestOp::DoDatabaseWork", DOM); DatabaseConnection::AutoSavepoint autoSave; QM_TRY(MOZ_TO_RESULT(autoSave.Start(Transaction())) #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED , QM_PROPAGATE, MakeAutoSavepointCleanupHandler(*aConnection) #endif ); QM_TRY_INSPECT(const bool& objectStoreHasIndexes, ObjectStoreHasIndexes(*aConnection, mParams.objectStoreId(), mObjectStoreMayHaveIndexes)); if (objectStoreHasIndexes) { QM_TRY(MOZ_TO_RESULT(DeleteObjectStoreDataTableRowsWithIndexes( aConnection, mParams.objectStoreId(), Some(mParams.keyRange())))); } else { const auto keyRangeClause = GetBindingClauseForKeyRange(mParams.keyRange(), kColumnNameKey); QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement( "DELETE FROM object_data " "WHERE object_store_id = :"_ns + kStmtParamNameObjectStoreId + keyRangeClause + ";"_ns, [¶ms = mParams]( mozIStorageStatement& stmt) -> mozilla::Result { QM_TRY(MOZ_TO_RESULT(stmt.BindInt64ByName(kStmtParamNameObjectStoreId, params.objectStoreId()))); QM_TRY( MOZ_TO_RESULT(BindKeyRangeToStatement(params.keyRange(), &stmt))); return Ok{}; }))); } QM_TRY(MOZ_TO_RESULT(autoSave.Commit())); return NS_OK; } ObjectStoreClearRequestOp::ObjectStoreClearRequestOp( SafeRefPtr aTransaction, const ObjectStoreClearParams& aParams) : NormalTransactionOp(std::move(aTransaction)), mParams(aParams), mObjectStoreMayHaveIndexes(false) { AssertIsOnBackgroundThread(); SafeRefPtr metadata = Transaction().GetMetadataForObjectStoreId(mParams.objectStoreId()); MOZ_ASSERT(metadata); mObjectStoreMayHaveIndexes = metadata->HasLiveIndexes(); } nsresult ObjectStoreClearRequestOp::DoDatabaseWork( DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); AUTO_PROFILER_LABEL("ObjectStoreClearRequestOp::DoDatabaseWork", DOM); DatabaseConnection::AutoSavepoint autoSave; QM_TRY(MOZ_TO_RESULT(autoSave.Start(Transaction())) #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED , QM_PROPAGATE, MakeAutoSavepointCleanupHandler(*aConnection) #endif ); QM_TRY_INSPECT(const bool& objectStoreHasIndexes, ObjectStoreHasIndexes(*aConnection, mParams.objectStoreId(), mObjectStoreMayHaveIndexes)); // The parameter names are not used, parameters are bound by index only // locally in the same function. QM_TRY(MOZ_TO_RESULT( objectStoreHasIndexes ? DeleteObjectStoreDataTableRowsWithIndexes( aConnection, mParams.objectStoreId(), Nothing()) : aConnection->ExecuteCachedStatement( "DELETE FROM object_data " "WHERE object_store_id = :object_store_id;"_ns, [objectStoreId = mParams.objectStoreId()](mozIStorageStatement& stmt) -> mozilla::Result { QM_TRY( MOZ_TO_RESULT(stmt.BindInt64ByIndex(0, objectStoreId))); return Ok{}; }))); QM_TRY(MOZ_TO_RESULT(autoSave.Commit())); return NS_OK; } nsresult ObjectStoreCountRequestOp::DoDatabaseWork( DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); AUTO_PROFILER_LABEL("ObjectStoreCountRequestOp::DoDatabaseWork", DOM); const auto keyRangeClause = MaybeGetBindingClauseForKeyRange( mParams.optionalKeyRange(), kColumnNameKey); QM_TRY_INSPECT( const auto& maybeStmt, aConnection->BorrowAndExecuteSingleStepStatement( "SELECT count(*) " "FROM object_data " "WHERE object_store_id = :"_ns + kStmtParamNameObjectStoreId + keyRangeClause, [¶ms = mParams](auto& stmt) -> mozilla::Result { QM_TRY(MOZ_TO_RESULT(stmt.BindInt64ByName( kStmtParamNameObjectStoreId, params.objectStoreId()))); if (params.optionalKeyRange().isSome()) { QM_TRY(MOZ_TO_RESULT(BindKeyRangeToStatement( params.optionalKeyRange().ref(), &stmt))); } return Ok{}; })); QM_TRY(OkIf(maybeStmt.isSome()), NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR, [](const auto) { // XXX Why do we have an assertion here, but not at most other // places using IDB_REPORT_INTERNAL_ERR(_LAMBDA)? MOZ_ASSERT(false, "This should never be possible!"); IDB_REPORT_INTERNAL_ERR(); }); const auto& stmt = *maybeStmt; const int64_t count = stmt->AsInt64(0); QM_TRY(OkIf(count >= 0), NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR, [](const auto) { // XXX Why do we have an assertion here, but not at most other places using // IDB_REPORT_INTERNAL_ERR(_LAMBDA)? MOZ_ASSERT(false, "This should never be possible!"); IDB_REPORT_INTERNAL_ERR(); }); mResponse.count() = count; return NS_OK; } // static SafeRefPtr IndexRequestOpBase::IndexMetadataForParams( const TransactionBase& aTransaction, const RequestParams& aParams) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aParams.type() == RequestParams::TIndexGetParams || aParams.type() == RequestParams::TIndexGetKeyParams || aParams.type() == RequestParams::TIndexGetAllParams || aParams.type() == RequestParams::TIndexGetAllKeysParams || aParams.type() == RequestParams::TIndexCountParams); IndexOrObjectStoreId objectStoreId; IndexOrObjectStoreId indexId; switch (aParams.type()) { case RequestParams::TIndexGetParams: { const IndexGetParams& params = aParams.get_IndexGetParams(); objectStoreId = params.objectStoreId(); indexId = params.indexId(); break; } case RequestParams::TIndexGetKeyParams: { const IndexGetKeyParams& params = aParams.get_IndexGetKeyParams(); objectStoreId = params.objectStoreId(); indexId = params.indexId(); break; } case RequestParams::TIndexGetAllParams: { const IndexGetAllParams& params = aParams.get_IndexGetAllParams(); objectStoreId = params.objectStoreId(); indexId = params.indexId(); break; } case RequestParams::TIndexGetAllKeysParams: { const IndexGetAllKeysParams& params = aParams.get_IndexGetAllKeysParams(); objectStoreId = params.objectStoreId(); indexId = params.indexId(); break; } case RequestParams::TIndexCountParams: { const IndexCountParams& params = aParams.get_IndexCountParams(); objectStoreId = params.objectStoreId(); indexId = params.indexId(); break; } default: MOZ_CRASH("Should never get here!"); } const SafeRefPtr objectStoreMetadata = aTransaction.GetMetadataForObjectStoreId(objectStoreId); MOZ_ASSERT(objectStoreMetadata); SafeRefPtr indexMetadata = aTransaction.GetMetadataForIndexId(*objectStoreMetadata, indexId); MOZ_ASSERT(indexMetadata); return indexMetadata; } IndexGetRequestOp::IndexGetRequestOp(SafeRefPtr aTransaction, const RequestParams& aParams, bool aGetAll) : IndexRequestOpBase(std::move(aTransaction), aParams), mDatabase(Transaction().GetDatabasePtr()), mOptionalKeyRange(aGetAll ? aParams.get_IndexGetAllParams().optionalKeyRange() : Some(aParams.get_IndexGetParams().keyRange())), mBackgroundParent(Transaction().GetBackgroundParent()), mLimit(aGetAll ? aParams.get_IndexGetAllParams().limit() : 1), mGetAll(aGetAll) { MOZ_ASSERT(aParams.type() == RequestParams::TIndexGetParams || aParams.type() == RequestParams::TIndexGetAllParams); MOZ_ASSERT(mDatabase); MOZ_ASSERT_IF(!aGetAll, mOptionalKeyRange.isSome()); MOZ_ASSERT(mBackgroundParent); } nsresult IndexGetRequestOp::DoDatabaseWork(DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); MOZ_ASSERT_IF(!mGetAll, mOptionalKeyRange.isSome()); MOZ_ASSERT_IF(!mGetAll, mLimit == 1); AUTO_PROFILER_LABEL("IndexGetRequestOp::DoDatabaseWork", DOM); const auto indexTable = mMetadata->mCommonMetadata.unique() ? "unique_index_data "_ns : "index_data "_ns; QM_TRY_INSPECT( const auto& stmt, aConnection->BorrowCachedStatement( "SELECT file_ids, data " "FROM object_data " "INNER JOIN "_ns + indexTable + "AS index_table " "ON object_data.object_store_id = " "index_table.object_store_id " "AND object_data.key = " "index_table.object_data_key " "WHERE index_id = :"_ns + kStmtParamNameIndexId + MaybeGetBindingClauseForKeyRange(mOptionalKeyRange, kColumnNameValue) + (mLimit ? kOpenLimit + IntToCString(mLimit) : EmptyCString()))); QM_TRY(MOZ_TO_RESULT(stmt->BindInt64ByName(kStmtParamNameIndexId, mMetadata->mCommonMetadata.id()))); if (mOptionalKeyRange.isSome()) { QM_TRY(MOZ_TO_RESULT( BindKeyRangeToStatement(mOptionalKeyRange.ref(), &*stmt))); } QM_TRY(CollectWhileHasResult( *stmt, [this](auto& stmt) mutable -> mozilla::Result { QM_TRY_UNWRAP(auto cloneInfo, GetStructuredCloneReadInfoFromStatement( &stmt, 1, 0, mDatabase->GetFileManager())); if (cloneInfo.HasPreprocessInfo()) { IDB_WARNING("Preprocessing for indexes not yet implemented!"); return Err(NS_ERROR_NOT_IMPLEMENTED); } QM_TRY(OkIf(mResponse.EmplaceBack(fallible, std::move(cloneInfo))), Err(NS_ERROR_OUT_OF_MEMORY)); return Ok{}; })); MOZ_ASSERT_IF(!mGetAll, mResponse.Length() <= 1); return NS_OK; } // XXX This is more or less a duplicate of ObjectStoreGetRequestOp::GetResponse void IndexGetRequestOp::GetResponse(RequestResponse& aResponse, size_t* aResponseSize) { MOZ_ASSERT_IF(!mGetAll, mResponse.Length() <= 1); auto convertResponse = [this](StructuredCloneReadInfoParent&& info) -> mozilla::Result { SerializedStructuredCloneReadInfo result; result.data().data = info.ReleaseData(); QM_TRY_UNWRAP(result.files(), SerializeStructuredCloneFiles( mDatabase, info.Files(), false)); return result; }; if (mGetAll) { aResponse = IndexGetAllResponse(); *aResponseSize = 0; if (!mResponse.IsEmpty()) { QM_TRY_UNWRAP( aResponse.get_IndexGetAllResponse().cloneInfos(), TransformIntoNewArrayAbortOnErr( std::make_move_iterator(mResponse.begin()), std::make_move_iterator(mResponse.end()), [convertResponse, &aResponseSize](StructuredCloneReadInfoParent&& info) { *aResponseSize += info.Size(); return convertResponse(std::move(info)); }, fallible), QM_VOID, [&aResponse](const nsresult result) { aResponse = result; }); } return; } aResponse = IndexGetResponse(); *aResponseSize = 0; if (!mResponse.IsEmpty()) { SerializedStructuredCloneReadInfo& serializedInfo = aResponse.get_IndexGetResponse().cloneInfo(); *aResponseSize += mResponse[0].Size(); QM_TRY_UNWRAP(serializedInfo, convertResponse(std::move(mResponse[0])), QM_VOID, [&aResponse](const nsresult result) { aResponse = result; }); } } IndexGetKeyRequestOp::IndexGetKeyRequestOp( SafeRefPtr aTransaction, const RequestParams& aParams, bool aGetAll) : IndexRequestOpBase(std::move(aTransaction), aParams), mOptionalKeyRange( aGetAll ? aParams.get_IndexGetAllKeysParams().optionalKeyRange() : Some(aParams.get_IndexGetKeyParams().keyRange())), mLimit(aGetAll ? aParams.get_IndexGetAllKeysParams().limit() : 1), mGetAll(aGetAll) { MOZ_ASSERT(aParams.type() == RequestParams::TIndexGetKeyParams || aParams.type() == RequestParams::TIndexGetAllKeysParams); MOZ_ASSERT_IF(!aGetAll, mOptionalKeyRange.isSome()); } nsresult IndexGetKeyRequestOp::DoDatabaseWork(DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); MOZ_ASSERT_IF(!mGetAll, mOptionalKeyRange.isSome()); MOZ_ASSERT_IF(!mGetAll, mLimit == 1); AUTO_PROFILER_LABEL("IndexGetKeyRequestOp::DoDatabaseWork", DOM); const bool hasKeyRange = mOptionalKeyRange.isSome(); const auto indexTable = mMetadata->mCommonMetadata.unique() ? "unique_index_data "_ns : "index_data "_ns; const nsCString query = "SELECT object_data_key " "FROM "_ns + indexTable + "WHERE index_id = :"_ns + kStmtParamNameIndexId + MaybeGetBindingClauseForKeyRange(mOptionalKeyRange, kColumnNameValue) + (mLimit ? kOpenLimit + IntToCString(mLimit) : EmptyCString()); QM_TRY_INSPECT(const auto& stmt, aConnection->BorrowCachedStatement(query)); QM_TRY(MOZ_TO_RESULT(stmt->BindInt64ByName(kStmtParamNameIndexId, mMetadata->mCommonMetadata.id()))); if (hasKeyRange) { QM_TRY(MOZ_TO_RESULT( BindKeyRangeToStatement(mOptionalKeyRange.ref(), &*stmt))); } QM_TRY(CollectWhileHasResult( *stmt, [this](auto& stmt) mutable -> mozilla::Result { Key* const key = mResponse.AppendElement(fallible); QM_TRY(OkIf(key), Err(NS_ERROR_OUT_OF_MEMORY)); QM_TRY(MOZ_TO_RESULT(key->SetFromStatement(&stmt, 0))); return Ok{}; })); MOZ_ASSERT_IF(!mGetAll, mResponse.Length() <= 1); return NS_OK; } void IndexGetKeyRequestOp::GetResponse(RequestResponse& aResponse, size_t* aResponseSize) { MOZ_ASSERT_IF(!mGetAll, mResponse.Length() <= 1); if (mGetAll) { aResponse = IndexGetAllKeysResponse(); *aResponseSize = std::accumulate(mResponse.begin(), mResponse.end(), 0u, [](size_t old, const auto& entry) { return old + entry.GetBuffer().Length(); }); aResponse.get_IndexGetAllKeysResponse().keys() = std::move(mResponse); return; } aResponse = IndexGetKeyResponse(); *aResponseSize = 0; if (!mResponse.IsEmpty()) { *aResponseSize = mResponse[0].GetBuffer().Length(); aResponse.get_IndexGetKeyResponse().key() = std::move(mResponse[0]); } } nsresult IndexCountRequestOp::DoDatabaseWork(DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); AUTO_PROFILER_LABEL("IndexCountRequestOp::DoDatabaseWork", DOM); const auto indexTable = mMetadata->mCommonMetadata.unique() ? "unique_index_data "_ns : "index_data "_ns; const auto keyRangeClause = MaybeGetBindingClauseForKeyRange( mParams.optionalKeyRange(), kColumnNameValue); QM_TRY_INSPECT( const auto& maybeStmt, aConnection->BorrowAndExecuteSingleStepStatement( "SELECT count(*) " "FROM "_ns + indexTable + "WHERE index_id = :"_ns + kStmtParamNameIndexId + keyRangeClause, [&self = *this](auto& stmt) -> mozilla::Result { QM_TRY(MOZ_TO_RESULT(stmt.BindInt64ByName( kStmtParamNameIndexId, self.mMetadata->mCommonMetadata.id()))); if (self.mParams.optionalKeyRange().isSome()) { QM_TRY(MOZ_TO_RESULT(BindKeyRangeToStatement( self.mParams.optionalKeyRange().ref(), &stmt))); } return Ok{}; })); QM_TRY(OkIf(maybeStmt.isSome()), NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR, [](const auto) { // XXX Why do we have an assertion here, but not at most other // places using IDB_REPORT_INTERNAL_ERR(_LAMBDA)? MOZ_ASSERT(false, "This should never be possible!"); IDB_REPORT_INTERNAL_ERR(); }); const auto& stmt = *maybeStmt; const int64_t count = stmt->AsInt64(0); QM_TRY(OkIf(count >= 0), NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR, [](const auto) { // XXX Why do we have an assertion here, but not at most other places using // IDB_REPORT_INTERNAL_ERR(_LAMBDA)? MOZ_ASSERT(false, "This should never be possible!"); IDB_REPORT_INTERNAL_ERR(); }); mResponse.count() = count; return NS_OK; } template bool Cursor::CursorOpBase::SendFailureResult(nsresult aResultCode) { AssertIsOnOwningThread(); MOZ_ASSERT(NS_FAILED(aResultCode)); MOZ_ASSERT(mCursor); MOZ_ASSERT(mCursor->mCurrentlyRunningOp == this); MOZ_ASSERT(!mResponseSent); if (!IsActorDestroyed()) { mResponse = ClampResultCode(aResultCode); // This is an expected race when the transaction is invalidated after // data is retrieved from database. // // TODO: There seem to be other cases when mFiles is non-empty here, which // have been present before adding cursor preloading, but with cursor // preloading they have become more frequent (also during startup). One // possible cause with cursor preloading is to be addressed by Bug 1597191. NS_WARNING_ASSERTION( !mFiles.IsEmpty() && !Transaction().IsInvalidated(), "Expected empty mFiles when transaction has not been invalidated"); // SendResponseInternal will assert when mResponse.type() is // CursorResponse::Tnsresult and mFiles is non-empty, so we clear mFiles // here. mFiles.Clear(); mCursor->SendResponseInternal(mResponse, mFiles); } #ifdef DEBUG mResponseSent = true; #endif return false; } template void Cursor::CursorOpBase::Cleanup() { AssertIsOnOwningThread(); MOZ_ASSERT(mCursor); MOZ_ASSERT_IF(!IsActorDestroyed(), mResponseSent); mCursor = nullptr; #ifdef DEBUG // A bit hacky but the CursorOp request is not generated in response to a // child request like most other database operations. Do this to make our // assertions happy. NoteActorDestroyed(); #endif TransactionDatabaseOperationBase::Cleanup(); } template ResponseSizeOrError CursorOpBaseHelperBase::PopulateResponseFromStatement( mozIStorageStatement* const aStmt, const bool aInitializeResponse, Key* const aOptOutSortKey) { mOp.Transaction().AssertIsOnConnectionThread(); MOZ_ASSERT_IF(aInitializeResponse, mOp.mResponse.type() == CursorResponse::T__None); MOZ_ASSERT_IF(!aInitializeResponse, mOp.mResponse.type() != CursorResponse::T__None); MOZ_ASSERT_IF( mOp.mFiles.IsEmpty() && (mOp.mResponse.type() == CursorResponse::TArrayOfObjectStoreCursorResponse || mOp.mResponse.type() == CursorResponse::TArrayOfIndexCursorResponse), aInitializeResponse); auto populateResponseHelper = PopulateResponseHelper{mOp}; auto previousKey = aOptOutSortKey ? std::move(*aOptOutSortKey) : Key{}; QM_TRY(MOZ_TO_RESULT(populateResponseHelper.GetKeys(aStmt, aOptOutSortKey))); // aOptOutSortKey must be set iff the cursor is a unique cursor. For unique // cursors, we need to skip records with the same key. The SQL queries // currently do not filter these out. if (aOptOutSortKey && !previousKey.IsUnset() && previousKey == *aOptOutSortKey) { return 0; } QM_TRY(MOZ_TO_RESULT( populateResponseHelper.MaybeGetCloneInfo(aStmt, GetCursor()))); // CAUTION: It is important that only the part of the function above this // comment may fail, and modifications to the data structure (in particular // mResponse and mFiles) may only be made below. This is necessary to allow to // discard entries that were attempted to be preloaded without causing an // inconsistent state. if (aInitializeResponse) { mOp.mResponse = std::remove_reference_t< decltype(populateResponseHelper.GetTypedResponse(&mOp.mResponse))>(); } auto& responses = populateResponseHelper.GetTypedResponse(&mOp.mResponse); auto& response = *responses.AppendElement(); populateResponseHelper.FillKeys(response); if constexpr (!CursorTypeTraits::IsKeyOnlyCursor) { populateResponseHelper.MaybeFillCloneInfo(response, &mOp.mFiles); } return populateResponseHelper.GetKeySize(response) + populateResponseHelper.MaybeGetCloneInfoSize(response); } template void CursorOpBaseHelperBase::PopulateExtraResponses( mozIStorageStatement* const aStmt, const uint32_t aMaxExtraCount, const size_t aInitialResponseSize, const nsACString& aOperation, Key* const aOptPreviousSortKey) { mOp.AssertIsOnConnectionThread(); const auto extraCount = [&]() -> uint32_t { auto accumulatedResponseSize = aInitialResponseSize; uint32_t extraCount = 0; do { bool hasResult; nsresult rv = aStmt->ExecuteStep(&hasResult); if (NS_WARN_IF(NS_FAILED(rv))) { // In case of a failure on one step, do not attempt to execute further // steps, but use the results already populated. break; } if (!hasResult) { break; } // PopulateResponseFromStatement does not modify the data in case of // failure, so we can just use the results already populated, and discard // any remaining entries, and signal overall success. Probably, future // attempts to access the same entry will fail as well, but it might never // be accessed by the application. QM_TRY_INSPECT( const auto& responseSize, PopulateResponseFromStatement(aStmt, false, aOptPreviousSortKey), extraCount, [](const auto&) { // TODO: Maybe disable preloading for this cursor? The problem will // probably reoccur on the next attempt, and disabling preloading // will reduce latency. However, if some problematic entry will be // skipped over, after that it might be fine again. To judge this, // the causes for such failures would need to be analyzed more // thoroughly. Since this seems to be rare, maybe no further action // is necessary at all. }); // Check accumulated size of individual responses and maybe break early. accumulatedResponseSize += responseSize; if (accumulatedResponseSize > IPC::Channel::kMaximumMessageSize / 2) { IDB_LOG_MARK_PARENT_TRANSACTION_REQUEST( "PRELOAD: %s: Dropping entries because maximum message size is " "exceeded: %" PRIu32 "/%zu bytes", "%.0s Dropping too large (%" PRIu32 "/%zu)", IDB_LOG_ID_STRING(mOp.mBackgroundChildLoggingId), mOp.mTransactionLoggingSerialNumber, mOp.mLoggingSerialNumber, PromiseFlatCString(aOperation).get(), extraCount, accumulatedResponseSize); break; } // TODO: Do not count entries skipped for unique cursors. ++extraCount; } while (true); return extraCount; }(); IDB_LOG_MARK_PARENT_TRANSACTION_REQUEST( "PRELOAD: %s: Number of extra results populated: %" PRIu32 "/%" PRIu32, "%.0s Populated (%" PRIu32 "/%" PRIu32 ")", IDB_LOG_ID_STRING(mOp.mBackgroundChildLoggingId), mOp.mTransactionLoggingSerialNumber, mOp.mLoggingSerialNumber, PromiseFlatCString(aOperation).get(), extraCount, aMaxExtraCount); } template void Cursor::SetOptionalKeyRange( const Maybe& aOptionalKeyRange, bool* const aOpen) { MOZ_ASSERT(aOpen); Key localeAwareRangeBound; if (aOptionalKeyRange.isSome()) { const SerializedKeyRange& range = aOptionalKeyRange.ref(); const bool lowerBound = !IsIncreasingOrder(mDirection); *aOpen = !range.isOnly() && (lowerBound ? range.lowerOpen() : range.upperOpen()); const auto& bound = (range.isOnly() || lowerBound) ? range.lower() : range.upper(); if constexpr (IsIndexCursor) { if (this->IsLocaleAware()) { // XXX Don't we need to propagate the error? QM_TRY_UNWRAP(localeAwareRangeBound, bound.ToLocaleAwareKey(this->mLocale), QM_VOID); } else { localeAwareRangeBound = bound; } } else { localeAwareRangeBound = bound; } } else { *aOpen = false; } this->mLocaleAwareRangeBound.init(std::move(localeAwareRangeBound)); } template void ObjectStoreOpenOpHelper::PrepareKeyConditionClauses( const nsACString& aDirectionClause, const nsACString& aQueryStart) { const bool isIncreasingOrder = IsIncreasingOrder(GetCursor().mDirection); nsAutoCString keyRangeClause; nsAutoCString continueToKeyRangeClause; AppendConditionClause(kStmtParamNameKey, kStmtParamNameCurrentKey, !isIncreasingOrder, false, keyRangeClause); AppendConditionClause(kStmtParamNameKey, kStmtParamNameCurrentKey, !isIncreasingOrder, true, continueToKeyRangeClause); { bool open; GetCursor().SetOptionalKeyRange(GetOptionalKeyRange(), &open); if (GetOptionalKeyRange().isSome() && !GetCursor().mLocaleAwareRangeBound->IsUnset()) { AppendConditionClause(kStmtParamNameKey, kStmtParamNameRangeBound, isIncreasingOrder, !open, keyRangeClause); AppendConditionClause(kStmtParamNameKey, kStmtParamNameRangeBound, isIncreasingOrder, !open, continueToKeyRangeClause); } } const nsAutoCString suffix = aDirectionClause + kOpenLimit + ":"_ns + kStmtParamNameLimit; GetCursor().mContinueQueries.init( aQueryStart + keyRangeClause + suffix, aQueryStart + continueToKeyRangeClause + suffix); } template void IndexOpenOpHelper::PrepareIndexKeyConditionClause( const nsACString& aDirectionClause, const nsLiteralCString& aObjectDataKeyPrefix, nsAutoCString aQueryStart) { const bool isIncreasingOrder = IsIncreasingOrder(GetCursor().mDirection); { bool open; GetCursor().SetOptionalKeyRange(GetOptionalKeyRange(), &open); if (GetOptionalKeyRange().isSome() && !GetCursor().mLocaleAwareRangeBound->IsUnset()) { AppendConditionClause(kColumnNameAliasSortKey, kStmtParamNameRangeBound, isIncreasingOrder, !open, aQueryStart); } } nsCString continueQuery, continueToQuery, continuePrimaryKeyQuery; continueToQuery = aQueryStart + " AND "_ns + GetSortKeyClause(isIncreasingOrder ? ComparisonOperator::GreaterOrEquals : ComparisonOperator::LessOrEquals, kStmtParamNameCurrentKey); switch (GetCursor().mDirection) { case IDBCursorDirection::Next: case IDBCursorDirection::Prev: continueQuery = aQueryStart + " AND "_ns + GetSortKeyClause(isIncreasingOrder ? ComparisonOperator::GreaterOrEquals : ComparisonOperator::LessOrEquals, kStmtParamNameCurrentKey) + " AND ( "_ns + GetSortKeyClause(isIncreasingOrder ? ComparisonOperator::GreaterThan : ComparisonOperator::LessThan, kStmtParamNameCurrentKey) + " OR "_ns + GetKeyClause(aObjectDataKeyPrefix + "object_data_key"_ns, isIncreasingOrder ? ComparisonOperator::GreaterThan : ComparisonOperator::LessThan, kStmtParamNameObjectStorePosition) + " ) "_ns; continuePrimaryKeyQuery = aQueryStart + " AND (" "("_ns + GetSortKeyClause(ComparisonOperator::Equals, kStmtParamNameCurrentKey) + " AND "_ns + GetKeyClause(aObjectDataKeyPrefix + "object_data_key"_ns, isIncreasingOrder ? ComparisonOperator::GreaterOrEquals : ComparisonOperator::LessOrEquals, kStmtParamNameObjectStorePosition) + ") OR "_ns + GetSortKeyClause(isIncreasingOrder ? ComparisonOperator::GreaterThan : ComparisonOperator::LessThan, kStmtParamNameCurrentKey) + ")"_ns; break; case IDBCursorDirection::Nextunique: case IDBCursorDirection::Prevunique: continueQuery = aQueryStart + " AND "_ns + GetSortKeyClause(isIncreasingOrder ? ComparisonOperator::GreaterThan : ComparisonOperator::LessThan, kStmtParamNameCurrentKey); break; default: MOZ_CRASH("Should never get here!"); } const nsAutoCString suffix = aDirectionClause + kOpenLimit + ":"_ns + kStmtParamNameLimit; continueQuery += suffix; continueToQuery += suffix; if (!continuePrimaryKeyQuery.IsEmpty()) { continuePrimaryKeyQuery += suffix; } GetCursor().mContinueQueries.init(std::move(continueQuery), std::move(continueToQuery), std::move(continuePrimaryKeyQuery)); } template nsresult CommonOpenOpHelper::ProcessStatementSteps( mozIStorageStatement* const aStmt) { QM_TRY_INSPECT(const bool& hasResult, MOZ_TO_RESULT_INVOKE_MEMBER(aStmt, ExecuteStep)); if (!hasResult) { SetResponse(void_t{}); return NS_OK; } Key previousKey; auto* optPreviousKey = IsUnique(GetCursor().mDirection) ? &previousKey : nullptr; QM_TRY_INSPECT(const auto& responseSize, PopulateResponseFromStatement(aStmt, true, optPreviousKey)); // The degree to which extra responses on OpenOp can actually be used depends // on the parameters of subsequent ContinueOp operations, see also comment in // ContinueOp::DoDatabaseWork. // // TODO: We should somehow evaluate the effects of this. Maybe use a smaller // extra count than for ContinueOp? PopulateExtraResponses(aStmt, GetCursor().mMaxExtraCount, responseSize, "OpenOp"_ns, optPreviousKey); return NS_OK; } nsresult OpenOpHelper::DoDatabaseWork( DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(GetCursor().mObjectStoreId); AUTO_PROFILER_LABEL("Cursor::OpenOp::DoObjectStoreDatabaseWork", DOM); const bool usingKeyRange = GetOptionalKeyRange().isSome(); const nsCString queryStart = "SELECT "_ns + kColumnNameKey + ", file_ids, data " "FROM object_data " "WHERE object_store_id = :"_ns + kStmtParamNameId; const auto keyRangeClause = DatabaseOperationBase::MaybeGetBindingClauseForKeyRange( GetOptionalKeyRange(), kColumnNameKey); const auto& directionClause = MakeDirectionClause(GetCursor().mDirection); // Note: Changing the number or order of SELECT columns in the query will // require changes to CursorOpBase::PopulateResponseFromStatement. const nsCString firstQuery = queryStart + keyRangeClause + directionClause + kOpenLimit + IntToCString(1 + GetCursor().mMaxExtraCount); QM_TRY_INSPECT(const auto& stmt, aConnection->BorrowCachedStatement(firstQuery)); QM_TRY(MOZ_TO_RESULT( stmt->BindInt64ByName(kStmtParamNameId, GetCursor().mObjectStoreId))); if (usingKeyRange) { QM_TRY(MOZ_TO_RESULT(DatabaseOperationBase::BindKeyRangeToStatement( GetOptionalKeyRange().ref(), &*stmt))); } // Now we need to make the query for ContinueOp. PrepareKeyConditionClauses(directionClause, queryStart); return ProcessStatementSteps(&*stmt); } nsresult OpenOpHelper::DoDatabaseWork( DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(GetCursor().mObjectStoreId); AUTO_PROFILER_LABEL("Cursor::OpenOp::DoObjectStoreKeyDatabaseWork", DOM); const bool usingKeyRange = GetOptionalKeyRange().isSome(); const nsCString queryStart = "SELECT "_ns + kColumnNameKey + " FROM object_data " "WHERE object_store_id = :"_ns + kStmtParamNameId; const auto keyRangeClause = DatabaseOperationBase::MaybeGetBindingClauseForKeyRange( GetOptionalKeyRange(), kColumnNameKey); const auto& directionClause = MakeDirectionClause(GetCursor().mDirection); // Note: Changing the number or order of SELECT columns in the query will // require changes to CursorOpBase::PopulateResponseFromStatement. const nsCString firstQuery = queryStart + keyRangeClause + directionClause + kOpenLimit + "1"_ns; QM_TRY_INSPECT(const auto& stmt, aConnection->BorrowCachedStatement(firstQuery)); QM_TRY(MOZ_TO_RESULT( stmt->BindInt64ByName(kStmtParamNameId, GetCursor().mObjectStoreId))); if (usingKeyRange) { QM_TRY(MOZ_TO_RESULT(DatabaseOperationBase::BindKeyRangeToStatement( GetOptionalKeyRange().ref(), &*stmt))); } // Now we need to make the query to get the next match. PrepareKeyConditionClauses(directionClause, queryStart); return ProcessStatementSteps(&*stmt); } nsresult OpenOpHelper::DoDatabaseWork( DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(GetCursor().mObjectStoreId); MOZ_ASSERT(GetCursor().mIndexId); AUTO_PROFILER_LABEL("Cursor::OpenOp::DoIndexDatabaseWork", DOM); const bool usingKeyRange = GetOptionalKeyRange().isSome(); const auto indexTable = GetCursor().mUniqueIndex ? "unique_index_data"_ns : "index_data"_ns; // The result of MakeColumnPairSelectionList is stored in a local variable, // since inlining it into the next statement causes a crash on some Mac OS X // builds (see https://bugzilla.mozilla.org/show_bug.cgi?id=1168606#c110). const auto columnPairSelectionList = MakeColumnPairSelectionList( "index_table.value"_ns, "index_table.value_locale"_ns, kColumnNameAliasSortKey, GetCursor().IsLocaleAware()); const nsCString sortColumnAlias = "SELECT "_ns + columnPairSelectionList + ", "_ns; const nsAutoCString queryStart = sortColumnAlias + "index_table.object_data_key, " "object_data.file_ids, " "object_data.data " "FROM "_ns + indexTable + " AS index_table " "JOIN object_data " "ON index_table.object_store_id = " "object_data.object_store_id " "AND index_table.object_data_key = " "object_data.key " "WHERE index_table.index_id = :"_ns + kStmtParamNameId; const auto keyRangeClause = DatabaseOperationBase::MaybeGetBindingClauseForKeyRange( GetOptionalKeyRange(), kColumnNameAliasSortKey); nsAutoCString directionClause = " ORDER BY "_ns + kColumnNameAliasSortKey; switch (GetCursor().mDirection) { case IDBCursorDirection::Next: case IDBCursorDirection::Nextunique: directionClause.AppendLiteral(" ASC, index_table.object_data_key ASC"); break; case IDBCursorDirection::Prev: directionClause.AppendLiteral(" DESC, index_table.object_data_key DESC"); break; case IDBCursorDirection::Prevunique: directionClause.AppendLiteral(" DESC, index_table.object_data_key ASC"); break; default: MOZ_CRASH("Should never get here!"); } // Note: Changing the number or order of SELECT columns in the query will // require changes to CursorOpBase::PopulateResponseFromStatement. const nsCString firstQuery = queryStart + keyRangeClause + directionClause + kOpenLimit + IntToCString(1 + GetCursor().mMaxExtraCount); QM_TRY_INSPECT(const auto& stmt, aConnection->BorrowCachedStatement(firstQuery)); QM_TRY(MOZ_TO_RESULT( stmt->BindInt64ByName(kStmtParamNameId, GetCursor().mIndexId))); if (usingKeyRange) { if (GetCursor().IsLocaleAware()) { QM_TRY(MOZ_TO_RESULT(DatabaseOperationBase::BindKeyRangeToStatement( GetOptionalKeyRange().ref(), &*stmt, GetCursor().mLocale))); } else { QM_TRY(MOZ_TO_RESULT(DatabaseOperationBase::BindKeyRangeToStatement( GetOptionalKeyRange().ref(), &*stmt))); } } // TODO: At least the last two statements are almost the same in all // DoDatabaseWork variants, consider removing this duplication. // Now we need to make the query to get the next match. PrepareKeyConditionClauses(directionClause, std::move(queryStart)); return ProcessStatementSteps(&*stmt); } nsresult OpenOpHelper::DoDatabaseWork( DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(GetCursor().mObjectStoreId); MOZ_ASSERT(GetCursor().mIndexId); AUTO_PROFILER_LABEL("Cursor::OpenOp::DoIndexKeyDatabaseWork", DOM); const bool usingKeyRange = GetOptionalKeyRange().isSome(); const auto table = GetCursor().mUniqueIndex ? "unique_index_data"_ns : "index_data"_ns; // The result of MakeColumnPairSelectionList is stored in a local variable, // since inlining it into the next statement causes a crash on some Mac OS X // builds (see https://bugzilla.mozilla.org/show_bug.cgi?id=1168606#c110). const auto columnPairSelectionList = MakeColumnPairSelectionList( "value"_ns, "value_locale"_ns, kColumnNameAliasSortKey, GetCursor().IsLocaleAware()); const nsCString sortColumnAlias = "SELECT "_ns + columnPairSelectionList + ", "_ns; const nsAutoCString queryStart = sortColumnAlias + "object_data_key " " FROM "_ns + table + " WHERE index_id = :"_ns + kStmtParamNameId; const auto keyRangeClause = DatabaseOperationBase::MaybeGetBindingClauseForKeyRange( GetOptionalKeyRange(), kColumnNameAliasSortKey); nsAutoCString directionClause = " ORDER BY "_ns + kColumnNameAliasSortKey; switch (GetCursor().mDirection) { case IDBCursorDirection::Next: case IDBCursorDirection::Nextunique: directionClause.AppendLiteral(" ASC, object_data_key ASC"); break; case IDBCursorDirection::Prev: directionClause.AppendLiteral(" DESC, object_data_key DESC"); break; case IDBCursorDirection::Prevunique: directionClause.AppendLiteral(" DESC, object_data_key ASC"); break; default: MOZ_CRASH("Should never get here!"); } // Note: Changing the number or order of SELECT columns in the query will // require changes to CursorOpBase::PopulateResponseFromStatement. const nsCString firstQuery = queryStart + keyRangeClause + directionClause + kOpenLimit + "1"_ns; QM_TRY_INSPECT(const auto& stmt, aConnection->BorrowCachedStatement(firstQuery)); QM_TRY(MOZ_TO_RESULT( stmt->BindInt64ByName(kStmtParamNameId, GetCursor().mIndexId))); if (usingKeyRange) { if (GetCursor().IsLocaleAware()) { QM_TRY(MOZ_TO_RESULT(DatabaseOperationBase::BindKeyRangeToStatement( GetOptionalKeyRange().ref(), &*stmt, GetCursor().mLocale))); } else { QM_TRY(MOZ_TO_RESULT(DatabaseOperationBase::BindKeyRangeToStatement( GetOptionalKeyRange().ref(), &*stmt))); } } // Now we need to make the query to get the next match. PrepareKeyConditionClauses(directionClause, std::move(queryStart)); return ProcessStatementSteps(&*stmt); } template nsresult Cursor::OpenOp::DoDatabaseWork( DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(mCursor); MOZ_ASSERT(!mCursor->mContinueQueries); AUTO_PROFILER_LABEL("Cursor::OpenOp::DoDatabaseWork", DOM); auto helper = OpenOpHelper{*this}; const auto rv = helper.DoDatabaseWork(aConnection); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } template nsresult Cursor::CursorOpBase::SendSuccessResult() { AssertIsOnOwningThread(); MOZ_ASSERT(mCursor); MOZ_ASSERT(mCursor->mCurrentlyRunningOp == this); MOZ_ASSERT(mResponse.type() != CursorResponse::T__None); if (IsActorDestroyed()) { return NS_ERROR_DOM_INDEXEDDB_ABORT_ERR; } mCursor->SendResponseInternal(mResponse, mFiles); #ifdef DEBUG mResponseSent = true; #endif return NS_OK; } template nsresult Cursor::ContinueOp::DoDatabaseWork( DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(mCursor); MOZ_ASSERT(mCursor->mObjectStoreId); MOZ_ASSERT(!mCursor->mContinueQueries->mContinueQuery.IsEmpty()); MOZ_ASSERT(!mCursor->mContinueQueries->mContinueToQuery.IsEmpty()); MOZ_ASSERT(!mCurrentPosition.mKey.IsUnset()); if constexpr (IsIndexCursor) { MOZ_ASSERT_IF( mCursor->mDirection == IDBCursorDirection::Next || mCursor->mDirection == IDBCursorDirection::Prev, !mCursor->mContinueQueries->mContinuePrimaryKeyQuery.IsEmpty()); MOZ_ASSERT(mCursor->mIndexId); MOZ_ASSERT(!mCurrentPosition.mObjectStoreKey.IsUnset()); } AUTO_PROFILER_LABEL("Cursor::ContinueOp::DoDatabaseWork", DOM); // We need to pick a query based on whether or not a key was passed to the // continue function. If not we'll grab the next item in the database that // is greater than (or less than, if we're running a PREV cursor) the current // key. If a key was passed we'll grab the next item in the database that is // greater than (or less than, if we're running a PREV cursor) or equal to the // key that was specified. // // TODO: The description above is not complete, it does not take account of // ContinuePrimaryKey nor Advance. // // Note: Changing the number or order of SELECT columns in the query will // require changes to CursorOpBase::PopulateResponseFromStatement. const uint32_t advanceCount = mParams.type() == CursorRequestParams::TAdvanceParams ? mParams.get_AdvanceParams().count() : 1; MOZ_ASSERT(advanceCount > 0); bool hasContinueKey = false; bool hasContinuePrimaryKey = false; auto explicitContinueKey = Key{}; switch (mParams.type()) { case CursorRequestParams::TContinueParams: if (!mParams.get_ContinueParams().key().IsUnset()) { hasContinueKey = true; explicitContinueKey = mParams.get_ContinueParams().key(); } break; case CursorRequestParams::TContinuePrimaryKeyParams: MOZ_ASSERT(!mParams.get_ContinuePrimaryKeyParams().key().IsUnset()); MOZ_ASSERT( !mParams.get_ContinuePrimaryKeyParams().primaryKey().IsUnset()); MOZ_ASSERT(mCursor->mDirection == IDBCursorDirection::Next || mCursor->mDirection == IDBCursorDirection::Prev); hasContinueKey = true; hasContinuePrimaryKey = true; explicitContinueKey = mParams.get_ContinuePrimaryKeyParams().key(); break; case CursorRequestParams::TAdvanceParams: break; default: MOZ_CRASH("Should never get here!"); } // TODO: Whether it makes sense to preload depends on the kind of the // subsequent operations, not of the current operation. We could assume that // the subsequent operations are: // - the same as the current operation (with the same parameter values) // - as above, except for Advance, where we assume the count will be 1 on the // next call // - basic operations (Advance with count 1 or Continue-without-key) // // For now, we implement the second option for now (which correspond to // !hasContinueKey). // // Based on that, we could in both cases either preload for any assumed // subsequent operations, or only for the basic operations. For now, we // preload only for an assumed basic operation. Other operations would require // more work on the client side for invalidation, and may not make any sense // at all. const uint32_t maxExtraCount = hasContinueKey ? 0 : mCursor->mMaxExtraCount; QM_TRY_INSPECT(const auto& stmt, aConnection->BorrowCachedStatement( mCursor->mContinueQueries->GetContinueQuery( hasContinueKey, hasContinuePrimaryKey))); QM_TRY(MOZ_TO_RESULT(stmt->BindUTF8StringByName( kStmtParamNameLimit, IntToCString(advanceCount + mCursor->mMaxExtraCount)))); QM_TRY(MOZ_TO_RESULT(stmt->BindInt64ByName(kStmtParamNameId, mCursor->Id()))); // Bind current key. const auto& continueKey = hasContinueKey ? explicitContinueKey : mCurrentPosition.GetSortKey(mCursor->IsLocaleAware()); QM_TRY(MOZ_TO_RESULT( continueKey.BindToStatement(&*stmt, kStmtParamNameCurrentKey))); // Bind range bound if it is specified. if (!mCursor->mLocaleAwareRangeBound->IsUnset()) { QM_TRY(MOZ_TO_RESULT(mCursor->mLocaleAwareRangeBound->BindToStatement( &*stmt, kStmtParamNameRangeBound))); } // Bind object store position if duplicates are allowed and we're not // continuing to a specific key. if constexpr (IsIndexCursor) { if (!hasContinueKey && (mCursor->mDirection == IDBCursorDirection::Next || mCursor->mDirection == IDBCursorDirection::Prev)) { QM_TRY(MOZ_TO_RESULT(mCurrentPosition.mObjectStoreKey.BindToStatement( &*stmt, kStmtParamNameObjectStorePosition))); } else if (hasContinuePrimaryKey) { QM_TRY(MOZ_TO_RESULT( mParams.get_ContinuePrimaryKeyParams().primaryKey().BindToStatement( &*stmt, kStmtParamNameObjectStorePosition))); } } // TODO: Why do we query the records we don't need and skip them here, rather // than using a OFFSET clause in the query? for (uint32_t index = 0; index < advanceCount; index++) { QM_TRY_INSPECT(const bool& hasResult, MOZ_TO_RESULT_INVOKE_MEMBER(&*stmt, ExecuteStep)); if (!hasResult) { mResponse = void_t(); return NS_OK; } } Key previousKey; auto* const optPreviousKey = IsUnique(mCursor->mDirection) ? &previousKey : nullptr; auto helper = CursorOpBaseHelperBase{*this}; QM_TRY_INSPECT(const auto& responseSize, helper.PopulateResponseFromStatement( &*stmt, true, optPreviousKey)); helper.PopulateExtraResponses(&*stmt, maxExtraCount, responseSize, "ContinueOp"_ns, optPreviousKey); return NS_OK; } Utils::Utils() #ifdef DEBUG : mActorDestroyed(false) #endif { AssertIsOnBackgroundThread(); } Utils::~Utils() { MOZ_ASSERT(mActorDestroyed); } void Utils::ActorDestroy(ActorDestroyReason aWhy) { AssertIsOnBackgroundThread(); MOZ_ASSERT(!mActorDestroyed); #ifdef DEBUG mActorDestroyed = true; #endif } mozilla::ipc::IPCResult Utils::RecvDeleteMe() { AssertIsOnBackgroundThread(); MOZ_ASSERT(!mActorDestroyed); QM_WARNONLY_TRY(OkIf(PBackgroundIndexedDBUtilsParent::Send__delete__(this))); return IPC_OK(); } mozilla::ipc::IPCResult Utils::RecvGetFileReferences( const PersistenceType& aPersistenceType, const nsACString& aOrigin, const nsAString& aDatabaseName, const int64_t& aFileId, int32_t* aRefCnt, int32_t* aDBRefCnt, bool* aResult) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aRefCnt); MOZ_ASSERT(aDBRefCnt); MOZ_ASSERT(aResult); MOZ_ASSERT(!mActorDestroyed); if (NS_WARN_IF(!IndexedDatabaseManager::Get())) { return IPC_FAIL(this, "No IndexedDatabaseManager active!"); } if (NS_WARN_IF(!QuotaManager::Get())) { return IPC_FAIL(this, "No QuotaManager active!"); } if (NS_WARN_IF(!StaticPrefs::dom_indexedDB_testing())) { return IPC_FAIL(this, "IndexedDB is not in testing mode!"); } if (NS_WARN_IF(!IsValidPersistenceType(aPersistenceType))) { return IPC_FAIL(this, "PersistenceType is not valid!"); } if (NS_WARN_IF(aOrigin.IsEmpty())) { return IPC_FAIL(this, "Origin is empty!"); } if (NS_WARN_IF(aDatabaseName.IsEmpty())) { return IPC_FAIL(this, "DatabaseName is empty!"); } if (NS_WARN_IF(aFileId == 0)) { return IPC_FAIL(this, "No FileId!"); } nsresult rv = DispatchAndReturnFileReferences(aPersistenceType, aOrigin, aDatabaseName, aFileId, aRefCnt, aDBRefCnt, aResult); if (NS_WARN_IF(NS_FAILED(rv))) { return IPC_FAIL(this, "DispatchAndReturnFileReferences failed!"); } return IPC_OK(); } #ifdef DEBUG NS_IMPL_ISUPPORTS(DEBUGThreadSlower, nsIThreadObserver) NS_IMETHODIMP DEBUGThreadSlower::OnDispatchedEvent() { MOZ_CRASH("Should never be called!"); } NS_IMETHODIMP DEBUGThreadSlower::OnProcessNextEvent(nsIThreadInternal* /* aThread */, bool /* aMayWait */) { return NS_OK; } NS_IMETHODIMP DEBUGThreadSlower::AfterProcessNextEvent(nsIThreadInternal* /* aThread */, bool /* aEventWasProcessed */) { MOZ_ASSERT(kDEBUGThreadSleepMS); MOZ_ALWAYS_TRUE(PR_Sleep(PR_MillisecondsToInterval(kDEBUGThreadSleepMS)) == PR_SUCCESS); return NS_OK; } #endif // DEBUG nsresult FileHelper::Init() { MOZ_ASSERT(!IsOnBackgroundThread()); auto fileDirectory = mFileManager->GetCheckedDirectory(); if (NS_WARN_IF(!fileDirectory)) { return NS_ERROR_FAILURE; } auto journalDirectory = mFileManager->EnsureJournalDirectory(); if (NS_WARN_IF(!journalDirectory)) { return NS_ERROR_FAILURE; } DebugOnly exists; MOZ_ASSERT(NS_SUCCEEDED(journalDirectory->Exists(&exists))); MOZ_ASSERT(exists); DebugOnly isDirectory; MOZ_ASSERT(NS_SUCCEEDED(journalDirectory->IsDirectory(&isDirectory))); MOZ_ASSERT(isDirectory); mFileDirectory.init(WrapNotNullUnchecked(std::move(fileDirectory))); mJournalDirectory.init(WrapNotNullUnchecked(std::move(journalDirectory))); return NS_OK; } nsCOMPtr FileHelper::GetFile(const DatabaseFileInfo& aFileInfo) { MOZ_ASSERT(!IsOnBackgroundThread()); return mFileManager->GetFileForId(mFileDirectory->get(), aFileInfo.Id()); } nsCOMPtr FileHelper::GetJournalFile( const DatabaseFileInfo& aFileInfo) { MOZ_ASSERT(!IsOnBackgroundThread()); return mFileManager->GetFileForId(mJournalDirectory->get(), aFileInfo.Id()); } nsresult FileHelper::CreateFileFromStream(nsIFile& aFile, nsIFile& aJournalFile, nsIInputStream& aInputStream, bool aCompress, const Maybe& aMaybeKey) { MOZ_ASSERT(!IsOnBackgroundThread()); QM_TRY_INSPECT(const auto& exists, MOZ_TO_RESULT_INVOKE_MEMBER(aFile, Exists)); // DOM blobs that are being stored in IDB are cached by calling // IDBDatabase::GetOrCreateFileActorForBlob. So if the same DOM blob is stored // again under a different key or in a different object store, we just add // a new reference instead of creating a new copy (all such stored blobs share // the same id). // However, it can happen that CreateFileFromStream failed due to quota // exceeded error and for some reason the orphaned file couldn't be deleted // immediately. Now, if the operation is being repeated, the DOM blob is // already cached, so it has the same file id which clashes with the orphaned // file. We could do some tricks to restore previous copy loop, but it's safer // to just delete the orphaned file and start from scratch. // This corner case is partially simulated in test_file_copy_failure.js if (exists) { QM_TRY_INSPECT(const auto& isFile, MOZ_TO_RESULT_INVOKE_MEMBER(aFile, IsFile)); QM_TRY(OkIf(isFile), NS_ERROR_FAILURE); QM_TRY_INSPECT(const auto& journalExists, MOZ_TO_RESULT_INVOKE_MEMBER(aJournalFile, Exists)); QM_TRY(OkIf(journalExists), NS_ERROR_FAILURE); QM_TRY_INSPECT(const auto& journalIsFile, MOZ_TO_RESULT_INVOKE_MEMBER(aJournalFile, IsFile)); QM_TRY(OkIf(journalIsFile), NS_ERROR_FAILURE); IDB_WARNING("Deleting orphaned file!"); QM_TRY(MOZ_TO_RESULT(mFileManager->SyncDeleteFile(aFile, aJournalFile))); } // Create a journal file first. QM_TRY(MOZ_TO_RESULT(aJournalFile.Create(nsIFile::NORMAL_FILE_TYPE, 0644))); // Now try to copy the stream. QM_TRY_UNWRAP(nsCOMPtr fileOutputStream, CreateFileOutputStream(mFileManager->Type(), mFileManager->OriginMetadata(), Client::IDB, &aFile)); AutoTArray buffer; const auto actualOutputStream = [aCompress, &aMaybeKey, &buffer, baseOutputStream = std::move(fileOutputStream)]() mutable -> nsCOMPtr { if (aMaybeKey) { baseOutputStream = MakeRefPtr>( std::move(baseOutputStream), kEncryptedStreamBlockSize, *aMaybeKey); } if (aCompress) { auto snappyOutputStream = MakeRefPtr(baseOutputStream); buffer.SetLength(snappyOutputStream->BlockSize()); return snappyOutputStream; } buffer.SetLength(kFileCopyBufferSize); return std::move(baseOutputStream); }(); QM_TRY(MOZ_TO_RESULT(SyncCopy(aInputStream, *actualOutputStream, buffer.Elements(), buffer.Length()))); return NS_OK; } class FileHelper::ReadCallback final : public nsIInputStreamCallback { public: NS_DECL_THREADSAFE_ISUPPORTS ReadCallback() : mMutex("ReadCallback::mMutex"), mCondVar(mMutex, "ReadCallback::mCondVar"), mInputAvailable(false) {} NS_IMETHOD OnInputStreamReady(nsIAsyncInputStream* aStream) override { mozilla::MutexAutoLock autolock(mMutex); mInputAvailable = true; mCondVar.Notify(); return NS_OK; } nsresult AsyncWait(nsIAsyncInputStream* aStream, uint32_t aBufferSize, nsIEventTarget* aTarget) { MOZ_ASSERT(aStream); mozilla::MutexAutoLock autolock(mMutex); nsresult rv = aStream->AsyncWait(this, 0, aBufferSize, aTarget); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } mInputAvailable = false; while (!mInputAvailable) { mCondVar.Wait(); } return NS_OK; } private: ~ReadCallback() = default; mozilla::Mutex mMutex MOZ_UNANNOTATED; mozilla::CondVar mCondVar; bool mInputAvailable; }; NS_IMPL_ADDREF(FileHelper::ReadCallback); NS_IMPL_RELEASE(FileHelper::ReadCallback); NS_INTERFACE_MAP_BEGIN(FileHelper::ReadCallback) NS_INTERFACE_MAP_ENTRY(nsIInputStreamCallback) NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIInputStreamCallback) NS_INTERFACE_MAP_END nsresult FileHelper::SyncRead(nsIInputStream& aInputStream, char* const aBuffer, const uint32_t aBufferSize, uint32_t* const aRead) { MOZ_ASSERT(!IsOnBackgroundThread()); // Let's try to read, directly. nsresult rv = aInputStream.Read(aBuffer, aBufferSize, aRead); if (NS_SUCCEEDED(rv) || rv != NS_BASE_STREAM_WOULD_BLOCK) { return rv; } // We need to proceed async. nsCOMPtr asyncStream = do_QueryInterface(&aInputStream); if (!asyncStream) { return rv; } if (!mReadCallback) { mReadCallback.init(MakeNotNull>()); } // We just need any thread with an event loop for receiving the // OnInputStreamReady callback. Let's use the I/O thread. nsCOMPtr target = do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); MOZ_ASSERT(target); rv = (*mReadCallback)->AsyncWait(asyncStream, aBufferSize, target); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return SyncRead(aInputStream, aBuffer, aBufferSize, aRead); } nsresult FileHelper::SyncCopy(nsIInputStream& aInputStream, nsIOutputStream& aOutputStream, char* const aBuffer, const uint32_t aBufferSize) { MOZ_ASSERT(!IsOnBackgroundThread()); AUTO_PROFILER_LABEL("FileHelper::SyncCopy", DOM); nsresult rv; do { uint32_t numRead; rv = SyncRead(aInputStream, aBuffer, aBufferSize, &numRead); if (NS_WARN_IF(NS_FAILED(rv))) { break; } if (!numRead) { break; } uint32_t numWrite; rv = aOutputStream.Write(aBuffer, numRead, &numWrite); if (rv == NS_ERROR_FILE_NO_DEVICE_SPACE) { rv = NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR; } if (NS_WARN_IF(NS_FAILED(rv))) { break; } if (NS_WARN_IF(numWrite != numRead)) { rv = NS_ERROR_FAILURE; break; } } while (true); if (NS_SUCCEEDED(rv)) { rv = aOutputStream.Flush(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } nsresult rv2 = aOutputStream.Close(); if (NS_WARN_IF(NS_FAILED(rv2))) { return NS_SUCCEEDED(rv) ? rv2 : rv; } return rv; } } // namespace dom::indexedDB } // namespace mozilla #undef IDB_MOBILE #undef IDB_DEBUG_LOG