/* -*- Mode: C++; tab-width: 2; 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 <algorithm> #include <stdint.h> // UINTPTR_MAX, uintptr_t #include "FileInfo.h" #include "FileManager.h" #include "IDBObjectStore.h" #include "IDBTransaction.h" #include "IndexedDatabase.h" #include "IndexedDatabaseInlines.h" #include "IndexedDatabaseManager.h" #include "js/StructuredClone.h" #include "js/Value.h" #include "jsapi.h" #include "KeyPath.h" #include "mozilla/Attributes.h" #include "mozilla/AppProcessChecker.h" #include "mozilla/AutoRestore.h" #include "mozilla/Casting.h" #include "mozilla/EndianUtils.h" #include "mozilla/ErrorNames.h" #include "mozilla/LazyIdleThread.h" #include "mozilla/Maybe.h" #include "mozilla/Preferences.h" #include "mozilla/Services.h" #include "mozilla/SnappyCompressOutputStream.h" #include "mozilla/SnappyUncompressInputStream.h" #include "mozilla/StaticPtr.h" #include "mozilla/storage.h" #include "mozilla/Unused.h" #include "mozilla/UniquePtrExtensions.h" #include "mozilla/dom/ContentParent.h" #include "mozilla/dom/File.h" #include "mozilla/dom/StructuredCloneTags.h" #include "mozilla/dom/TabParent.h" #include "mozilla/dom/filehandle/ActorsParent.h" #include "mozilla/dom/indexedDB/PBackgroundIDBCursorParent.h" #include "mozilla/dom/indexedDB/PBackgroundIDBDatabaseParent.h" #include "mozilla/dom/indexedDB/PBackgroundIDBDatabaseFileParent.h" #include "mozilla/dom/indexedDB/PBackgroundIDBDatabaseRequestParent.h" #include "mozilla/dom/indexedDB/PBackgroundIDBFactoryParent.h" #include "mozilla/dom/indexedDB/PBackgroundIDBFactoryRequestParent.h" #include "mozilla/dom/indexedDB/PBackgroundIDBRequestParent.h" #include "mozilla/dom/indexedDB/PBackgroundIDBTransactionParent.h" #include "mozilla/dom/indexedDB/PBackgroundIDBVersionChangeTransactionParent.h" #include "mozilla/dom/indexedDB/PBackgroundIndexedDBUtilsParent.h" #include "mozilla/dom/indexedDB/PIndexedDBPermissionRequestParent.h" #include "mozilla/dom/ipc/BlobParent.h" #include "mozilla/dom/quota/Client.h" #include "mozilla/dom/quota/FileStreams.h" #include "mozilla/dom/quota/OriginScope.h" #include "mozilla/dom/quota/QuotaManager.h" #include "mozilla/dom/quota/UsageInfo.h" #include "mozilla/ipc/BackgroundParent.h" #include "mozilla/ipc/BackgroundUtils.h" #include "mozilla/ipc/InputStreamParams.h" #include "mozilla/ipc/InputStreamUtils.h" #include "mozilla/ipc/PBackground.h" #include "mozilla/Scoped.h" #include "mozilla/storage/Variant.h" #include "nsAutoPtr.h" #include "nsCharSeparatedTokenizer.h" #include "nsClassHashtable.h" #include "nsCOMPtr.h" #include "nsDataHashtable.h" #include "nsEscape.h" #include "nsHashKeys.h" #include "nsNetUtil.h" #include "nsISimpleEnumerator.h" #include "nsIAppsService.h" #include "nsIEventTarget.h" #include "nsIFile.h" #include "nsIFileURL.h" #include "nsIFileProtocolHandler.h" #include "nsIInputStream.h" #include "nsIInterfaceRequestor.h" #include "nsInterfaceHashtable.h" #include "nsIOutputStream.h" #include "nsIPipe.h" #include "nsIPrincipal.h" #include "nsIScriptSecurityManager.h" #include "nsISupports.h" #include "nsISupportsImpl.h" #include "nsISupportsPriority.h" #include "nsIThread.h" #include "nsITimer.h" #include "nsIURI.h" #include "nsNetUtil.h" #include "nsPrintfCString.h" #include "nsQueryObject.h" #include "nsRefPtrHashtable.h" #include "nsStreamUtils.h" #include "nsString.h" #include "nsStringStream.h" #include "nsThreadPool.h" #include "nsThreadUtils.h" #include "nsXPCOMCID.h" #include "PermissionRequestBase.h" #include "ProfilerHelpers.h" #include "prsystem.h" #include "prtime.h" #include "ReportInternalError.h" #include "snappy/snappy.h" #define DISABLE_ASSERTS_FOR_FUZZING 0 #if DISABLE_ASSERTS_FOR_FUZZING #define ASSERT_UNLESS_FUZZING(...) do { } while (0) #else #define ASSERT_UNLESS_FUZZING(...) MOZ_ASSERT(false, __VA_ARGS__) #endif #define IDB_DEBUG_LOG(_args) \ MOZ_LOG(IndexedDatabaseManager::GetLoggingModule(), \ LogLevel::Debug, \ _args ) #if defined(MOZ_WIDGET_ANDROID) || defined(MOZ_WIDGET_GONK) #define IDB_MOBILE #endif #define BLOB_IMPL_STORED_FILE_IID \ {0x6b505c84, 0x2c60, 0x4ffb, {0x8b, 0x91, 0xfe, 0x22, 0xb1, 0xec, 0x75, 0xe2}} namespace mozilla { MOZ_TYPE_SPECIFIC_SCOPED_POINTER_TEMPLATE(ScopedPRFileDesc, PRFileDesc, PR_Close); namespace dom { namespace indexedDB { using namespace mozilla::dom::quota; using namespace mozilla::ipc; namespace { class ConnectionPool; class Cursor; class Database; struct DatabaseActorInfo; class DatabaseFile; class DatabaseLoggingInfo; class DatabaseMaintenance; class Factory; class Maintenance; class MutableFile; class OpenDatabaseOp; class TransactionBase; class TransactionDatabaseOperationBase; class VersionChangeTransaction; /******************************************************************************* * Constants ******************************************************************************/ // If JS_STRUCTURED_CLONE_VERSION changes then we need to update our major // schema version. static_assert(JS_STRUCTURED_CLONE_VERSION == 8, "Need to update the major schema version."); // Major schema version. Bump for almost everything. const uint32_t kMajorSchemaVersion = 25; // Minor schema version. Should almost always be 0 (maybe bump on release // branches if we have to). const uint32_t kMinorSchemaVersion = 0; // The schema version we store in the SQLite database is a (signed) 32-bit // integer. The major version is left-shifted 4 bits so the max value is // 0xFFFFFFF. The minor version occupies the lower 4 bits and its max is 0xF. static_assert(kMajorSchemaVersion <= 0xFFFFFFF, "Major version needs to fit in 28 bits."); static_assert(kMinorSchemaVersion <= 0xF, "Minor version needs to fit in 4 bits."); const int32_t kSQLiteSchemaVersion = int32_t((kMajorSchemaVersion << 4) + kMinorSchemaVersion); 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;" const uint32_t kFileCopyBufferSize = 32768; #define JOURNAL_DIRECTORY_NAME "journals" const char kFileManagerDirectoryNameSuffix[] = ".files"; const char kSQLiteJournalSuffix[] = ".sqlite-journal"; const char kSQLiteSHMSuffix[] = ".sqlite-shm"; const char kSQLiteWALSuffix[] = ".sqlite-wal"; const char kPrefIndexedDBEnabled[] = "dom.indexedDB.enabled"; const char kPrefFileHandleEnabled[] = "dom.fileHandle.enabled"; #define IDB_PREFIX "indexedDB" #define PERMISSION_STRING_CHROME_BASE IDB_PREFIX "-chrome-" #define PERMISSION_STRING_CHROME_READ_SUFFIX "-read" #define PERMISSION_STRING_CHROME_WRITE_SUFFIX "-write" #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 template <size_t N> constexpr size_t LiteralStringLength(const char (&aArr)[N]) { static_assert(N, "Zero-length string literal?!"); // Don't include the null terminator. return N - 1; } /******************************************************************************* * Metadata classes ******************************************************************************/ struct FullIndexMetadata { IndexMetadata mCommonMetadata; bool mDeleted; public: FullIndexMetadata() : mCommonMetadata(0, nsString(), KeyPath(0), nsCString(), false, false, false) , mDeleted(false) { // This can happen either on the QuotaManager IO thread or on a // versionchange transaction thread. These threads can never race so this is // totally safe. } NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FullIndexMetadata) private: ~FullIndexMetadata() { } }; typedef nsRefPtrHashtable<nsUint64HashKey, FullIndexMetadata> IndexTable; struct FullObjectStoreMetadata { ObjectStoreMetadata mCommonMetadata; IndexTable mIndexes; // These two members are only ever touched on a transaction thread! int64_t mNextAutoIncrementId; int64_t mCommittedAutoIncrementId; bool mDeleted; public: FullObjectStoreMetadata() : mCommonMetadata(0, nsString(), KeyPath(0), false) , mNextAutoIncrementId(0) , mCommittedAutoIncrementId(0) , mDeleted(false) { // This can happen either on the QuotaManager IO thread or on a // versionchange transaction thread. These threads can never race so this is // totally safe. } NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FullObjectStoreMetadata); bool HasLiveIndexes() const; private: ~FullObjectStoreMetadata() { } }; typedef nsRefPtrHashtable<nsUint64HashKey, FullObjectStoreMetadata> ObjectStoreTable; struct FullDatabaseMetadata { DatabaseMetadata mCommonMetadata; nsCString mDatabaseId; nsString mFilePath; ObjectStoreTable mObjectStores; int64_t mNextObjectStoreId; int64_t mNextIndexId; public: explicit FullDatabaseMetadata(const DatabaseMetadata& aCommonMetadata) : mCommonMetadata(aCommonMetadata) , mNextObjectStoreId(0) , mNextIndexId(0) { AssertIsOnBackgroundThread(); } NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FullDatabaseMetadata) already_AddRefed<FullDatabaseMetadata> Duplicate() const; private: ~FullDatabaseMetadata() { } }; template <class MetadataType> class MOZ_STACK_CLASS MetadataNameOrIdMatcher final { typedef MetadataNameOrIdMatcher<MetadataType> SelfType; const int64_t mId; const nsString mName; RefPtr<MetadataType> mMetadata; bool mCheckName; public: template <class Enumerable> static MetadataType* Match(const Enumerable& aEnumerable, uint64_t aId, const nsAString& aName) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aId); SelfType closure(aId, aName); MatchHelper(aEnumerable, &closure); return closure.mMetadata; } template <class Enumerable> static MetadataType* Match(const Enumerable& aEnumerable, uint64_t aId) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aId); SelfType closure(aId); MatchHelper(aEnumerable, &closure); return closure.mMetadata; } private: MetadataNameOrIdMatcher(const int64_t& aId, const nsAString& aName) : mId(aId) , mName(PromiseFlatString(aName)) , mMetadata(nullptr) , mCheckName(true) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aId); } explicit MetadataNameOrIdMatcher(const int64_t& aId) : mId(aId) , mMetadata(nullptr) , mCheckName(false) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aId); } template <class Enumerable> static void MatchHelper(const Enumerable& aEnumerable, SelfType* aClosure) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aClosure); for (auto iter = aEnumerable.ConstIter(); !iter.Done(); iter.Next()) { #ifdef DEBUG const uint64_t key = iter.Key(); #endif MetadataType* value = iter.UserData(); MOZ_ASSERT(key != 0); MOZ_ASSERT(value); if (!value->mDeleted && (aClosure->mId == value->mCommonMetadata.id() || (aClosure->mCheckName && aClosure->mName == value->mCommonMetadata.name()))) { aClosure->mMetadata = value; break; } } } }; struct IndexDataValue final { int64_t mIndexId; Key mKey; Key mSortKey; bool mUnique; IndexDataValue() : mIndexId(0) , mUnique(false) { MOZ_COUNT_CTOR(IndexDataValue); } explicit IndexDataValue(const IndexDataValue& aOther) : mIndexId(aOther.mIndexId) , mKey(aOther.mKey) , mSortKey(aOther.mSortKey) , mUnique(aOther.mUnique) { MOZ_ASSERT(!aOther.mKey.IsUnset()); MOZ_COUNT_CTOR(IndexDataValue); } IndexDataValue(int64_t aIndexId, bool aUnique, const Key& aKey) : mIndexId(aIndexId) , mKey(aKey) , mUnique(aUnique) { MOZ_ASSERT(!aKey.IsUnset()); MOZ_COUNT_CTOR(IndexDataValue); } IndexDataValue(int64_t aIndexId, bool aUnique, const Key& aKey, const Key& aSortKey) : mIndexId(aIndexId) , mKey(aKey) , mSortKey(aSortKey) , mUnique(aUnique) { MOZ_ASSERT(!aKey.IsUnset()); MOZ_COUNT_CTOR(IndexDataValue); } ~IndexDataValue() { MOZ_COUNT_DTOR(IndexDataValue); } bool operator==(const IndexDataValue& aOther) const { if (mIndexId != aOther.mIndexId) { return false; } if (mSortKey.IsUnset()) { return mKey == aOther.mKey; } return mSortKey == aOther.mSortKey; } bool operator<(const IndexDataValue& aOther) const { if (mIndexId == aOther.mIndexId) { if (mSortKey.IsUnset()) { return mKey < aOther.mKey; } return mSortKey < aOther.mSortKey; } return mIndexId < aOther.mIndexId; } }; /******************************************************************************* * SQLite functions ******************************************************************************/ int32_t MakeSchemaVersion(uint32_t aMajorSchemaVersion, uint32_t aMinorSchemaVersion) { return int32_t((aMajorSchemaVersion << 4) + aMinorSchemaVersion); } 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; const char16_t* str = aName.BeginReading(); size_t length = aName.Length(); uint32_t hash = 0; for (size_t i = 0; i < length; i++) { hash = kGoldenRatioU32 * (Helper::RotateBitsLeft32(hash, 5) ^ str[i]); } return hash; } 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%X) to " "NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR", aResultCode); NS_WARNING(message.get()); #else ; #endif } IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } void GetDatabaseFilename(const nsAString& aName, nsAutoString& aDatabaseFilename) { MOZ_ASSERT(aDatabaseFilename.IsEmpty()); aDatabaseFilename.AppendInt(HashName(aName)); nsCString escapedName; if (!NS_Escape(NS_ConvertUTF16toUTF8(aName), 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++); } } aDatabaseFilename.AppendASCII(substring.get(), substring.Length()); } uint32_t CompressedByteCountForNumber(uint64_t aNumber) { // All bytes have 7 bits available. uint32_t count = 1; while ((aNumber >>= 7)) { count++; } return count; } uint32_t CompressedByteCountForIndexId(int64_t aIndexId) { MOZ_ASSERT(aIndexId); MOZ_ASSERT(UINT64_MAX - uint64_t(aIndexId) >= uint64_t(aIndexId), "Overflow!"); return CompressedByteCountForNumber(uint64_t(aIndexId * 2)); } void WriteCompressedNumber(uint64_t aNumber, uint8_t** aIterator) { MOZ_ASSERT(aIterator); MOZ_ASSERT(*aIterator); uint8_t*& buffer = *aIterator; #ifdef DEBUG const uint8_t* bufferStart = buffer; const uint64_t originalNumber = aNumber; #endif while (true) { uint64_t shiftedNumber = aNumber >> 7; if (shiftedNumber) { *buffer++ = uint8_t(0x80 | (aNumber & 0x7f)); aNumber = shiftedNumber; } else { *buffer++ = uint8_t(aNumber); break; } } MOZ_ASSERT(buffer > bufferStart); MOZ_ASSERT(uint32_t(buffer - bufferStart) == CompressedByteCountForNumber(originalNumber)); } uint64_t ReadCompressedNumber(const uint8_t** aIterator, const uint8_t* aEnd) { MOZ_ASSERT(aIterator); MOZ_ASSERT(*aIterator); MOZ_ASSERT(aEnd); MOZ_ASSERT(*aIterator < aEnd); const uint8_t*& buffer = *aIterator; uint8_t shiftCounter = 0; uint64_t result = 0; while (true) { MOZ_ASSERT(shiftCounter <= 56, "Shifted too many bits!"); result += (uint64_t(*buffer & 0x7f) << shiftCounter); shiftCounter += 7; if (!(*buffer++ & 0x80)) { break; } if (NS_WARN_IF(buffer == aEnd)) { MOZ_ASSERT(false); break; } } return result; } void WriteCompressedIndexId(int64_t aIndexId, bool aUnique, uint8_t** aIterator) { MOZ_ASSERT(aIndexId); MOZ_ASSERT(UINT64_MAX - uint64_t(aIndexId) >= uint64_t(aIndexId), "Overflow!"); MOZ_ASSERT(aIterator); MOZ_ASSERT(*aIterator); const uint64_t indexId = (uint64_t(aIndexId * 2) | (aUnique ? 1 : 0)); WriteCompressedNumber(indexId, aIterator); } void ReadCompressedIndexId(const uint8_t** aIterator, const uint8_t* aEnd, int64_t* aIndexId, bool* aUnique) { MOZ_ASSERT(aIterator); MOZ_ASSERT(*aIterator); MOZ_ASSERT(aIndexId); MOZ_ASSERT(aUnique); uint64_t indexId = ReadCompressedNumber(aIterator, aEnd); if (indexId % 2) { *aUnique = true; indexId--; } else { *aUnique = false; } MOZ_ASSERT(UINT64_MAX / 2 >= uint64_t(indexId), "Bad index id!"); *aIndexId = int64_t(indexId / 2); } // static nsresult MakeCompressedIndexDataValues( const FallibleTArray<IndexDataValue>& aIndexValues, UniqueFreePtr<uint8_t>& aCompressedIndexDataValues, uint32_t* aCompressedIndexDataValuesLength) { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(!aCompressedIndexDataValues); MOZ_ASSERT(aCompressedIndexDataValuesLength); PROFILER_LABEL("IndexedDB", "MakeCompressedIndexDataValues", js::ProfileEntry::Category::STORAGE); const uint32_t arrayLength = aIndexValues.Length(); if (!arrayLength) { *aCompressedIndexDataValuesLength = 0; return NS_OK; } // First calculate the size of the final buffer. uint32_t blobDataLength = 0; for (uint32_t arrayIndex = 0; arrayIndex < arrayLength; arrayIndex++) { const IndexDataValue& info = aIndexValues[arrayIndex]; const nsCString& keyBuffer = info.mKey.GetBuffer(); const nsCString& sortKeyBuffer = info.mSortKey.GetBuffer(); const uint32_t keyBufferLength = keyBuffer.Length(); const uint32_t sortKeyBufferLength = sortKeyBuffer.Length(); MOZ_ASSERT(!keyBuffer.IsEmpty()); // Don't let |infoLength| overflow. if (NS_WARN_IF(UINT32_MAX - keyBuffer.Length() < CompressedByteCountForIndexId(info.mIndexId) + CompressedByteCountForNumber(keyBufferLength) + CompressedByteCountForNumber(sortKeyBufferLength))) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } const uint32_t infoLength = CompressedByteCountForIndexId(info.mIndexId) + CompressedByteCountForNumber(keyBufferLength) + CompressedByteCountForNumber(sortKeyBufferLength) + keyBufferLength + sortKeyBufferLength; // Don't let |blobDataLength| overflow. if (NS_WARN_IF(UINT32_MAX - infoLength < blobDataLength)) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } blobDataLength += infoLength; } UniqueFreePtr<uint8_t> blobData( static_cast<uint8_t*>(malloc(blobDataLength))); if (NS_WARN_IF(!blobData)) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_OUT_OF_MEMORY; } uint8_t* blobDataIter = blobData.get(); for (uint32_t arrayIndex = 0; arrayIndex < arrayLength; arrayIndex++) { const IndexDataValue& info = aIndexValues[arrayIndex]; const nsCString& keyBuffer = info.mKey.GetBuffer(); const nsCString& sortKeyBuffer = info.mSortKey.GetBuffer(); const uint32_t keyBufferLength = keyBuffer.Length(); const uint32_t sortKeyBufferLength = sortKeyBuffer.Length(); WriteCompressedIndexId(info.mIndexId, info.mUnique, &blobDataIter); WriteCompressedNumber(keyBufferLength, &blobDataIter); memcpy(blobDataIter, keyBuffer.get(), keyBufferLength); blobDataIter += keyBufferLength; WriteCompressedNumber(sortKeyBufferLength, &blobDataIter); memcpy(blobDataIter, sortKeyBuffer.get(), sortKeyBufferLength); blobDataIter += sortKeyBufferLength; } MOZ_ASSERT(blobDataIter == blobData.get() + blobDataLength); aCompressedIndexDataValues.swap(blobData); *aCompressedIndexDataValuesLength = uint32_t(blobDataLength); return NS_OK; } nsresult ReadCompressedIndexDataValuesFromBlob(const uint8_t* aBlobData, uint32_t aBlobDataLength, nsTArray<IndexDataValue>& aIndexValues) { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(aBlobData); MOZ_ASSERT(aBlobDataLength); MOZ_ASSERT(aIndexValues.IsEmpty()); PROFILER_LABEL("IndexedDB", "ReadCompressedIndexDataValuesFromBlob", js::ProfileEntry::Category::STORAGE); if (uintptr_t(aBlobData) > UINTPTR_MAX - aBlobDataLength) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_FILE_CORRUPTED; } const uint8_t* blobDataIter = aBlobData; const uint8_t* blobDataEnd = aBlobData + aBlobDataLength; while (blobDataIter < blobDataEnd) { int64_t indexId; bool unique; ReadCompressedIndexId(&blobDataIter, blobDataEnd, &indexId, &unique); if (NS_WARN_IF(blobDataIter == blobDataEnd)) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_FILE_CORRUPTED; } // Read key buffer length. const uint64_t keyBufferLength = ReadCompressedNumber(&blobDataIter, blobDataEnd); if (NS_WARN_IF(blobDataIter == blobDataEnd) || NS_WARN_IF(keyBufferLength > uint64_t(UINT32_MAX)) || NS_WARN_IF(keyBufferLength > uintptr_t(blobDataEnd)) || NS_WARN_IF(blobDataIter > blobDataEnd - keyBufferLength)) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_FILE_CORRUPTED; } nsCString keyBuffer(reinterpret_cast<const char*>(blobDataIter), uint32_t(keyBufferLength)); blobDataIter += keyBufferLength; IndexDataValue idv(indexId, unique, Key(keyBuffer)); // Read sort key buffer length. const uint64_t sortKeyBufferLength = ReadCompressedNumber(&blobDataIter, blobDataEnd); if (sortKeyBufferLength > 0) { if (NS_WARN_IF(blobDataIter == blobDataEnd) || NS_WARN_IF(sortKeyBufferLength > uint64_t(UINT32_MAX)) || NS_WARN_IF(sortKeyBufferLength > uintptr_t(blobDataEnd)) || NS_WARN_IF(blobDataIter > blobDataEnd - sortKeyBufferLength)) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_FILE_CORRUPTED; } nsCString sortKeyBuffer(reinterpret_cast<const char*>(blobDataIter), uint32_t(sortKeyBufferLength)); blobDataIter += sortKeyBufferLength; idv.mSortKey = Key(sortKeyBuffer); } if (NS_WARN_IF(!aIndexValues.InsertElementSorted(idv, fallible))) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_OUT_OF_MEMORY; } } MOZ_ASSERT(blobDataIter == blobDataEnd); return NS_OK; } // static template <typename T> nsresult ReadCompressedIndexDataValuesFromSource(T* aSource, uint32_t aColumnIndex, nsTArray<IndexDataValue>& aIndexValues) { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(aSource); MOZ_ASSERT(aIndexValues.IsEmpty()); int32_t columnType; nsresult rv = aSource->GetTypeOfIndex(aColumnIndex, &columnType); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (columnType == mozIStorageStatement::VALUE_TYPE_NULL) { return NS_OK; } MOZ_ASSERT(columnType == mozIStorageStatement::VALUE_TYPE_BLOB); const uint8_t* blobData; uint32_t blobDataLength; rv = aSource->GetSharedBlob(aColumnIndex, &blobDataLength, &blobData); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (NS_WARN_IF(!blobDataLength)) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_FILE_CORRUPTED; } rv = ReadCompressedIndexDataValuesFromBlob(blobData, blobDataLength, aIndexValues); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } nsresult ReadCompressedIndexDataValues(mozIStorageStatement* aStatement, uint32_t aColumnIndex, nsTArray<IndexDataValue>& aIndexValues) { return ReadCompressedIndexDataValuesFromSource(aStatement, aColumnIndex, aIndexValues); } nsresult ReadCompressedIndexDataValues(mozIStorageValueArray* aValues, uint32_t aColumnIndex, nsTArray<IndexDataValue>& aIndexValues) { return ReadCompressedIndexDataValuesFromSource(aValues, aColumnIndex, aIndexValues); } nsresult CreateFileTables(mozIStorageConnection* aConnection) { AssertIsOnIOThread(); MOZ_ASSERT(aConnection); PROFILER_LABEL("IndexedDB", "CreateFileTables", js::ProfileEntry::Category::STORAGE); // Table `file` nsresult rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE file (" "id INTEGER PRIMARY KEY, " "refcount INTEGER NOT NULL" ");" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TRIGGER object_data_insert_trigger " "AFTER INSERT ON object_data " "FOR EACH ROW " "WHEN NEW.file_ids IS NOT NULL " "BEGIN " "SELECT update_refcount(NULL, NEW.file_ids); " "END;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TRIGGER object_data_update_trigger " "AFTER UPDATE OF file_ids ON object_data " "FOR EACH ROW " "WHEN OLD.file_ids IS NOT NULL OR NEW.file_ids IS NOT NULL " "BEGIN " "SELECT update_refcount(OLD.file_ids, NEW.file_ids); " "END;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TRIGGER object_data_delete_trigger " "AFTER DELETE ON object_data " "FOR EACH ROW WHEN OLD.file_ids IS NOT NULL " "BEGIN " "SELECT update_refcount(OLD.file_ids, NULL); " "END;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TRIGGER file_update_trigger " "AFTER UPDATE ON file " "FOR EACH ROW WHEN NEW.refcount = 0 " "BEGIN " "DELETE FROM file WHERE id = OLD.id; " "END;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } nsresult CreateTables(mozIStorageConnection* aConnection) { AssertIsOnIOThread(); MOZ_ASSERT(aConnection); PROFILER_LABEL("IndexedDB", "CreateTables", js::ProfileEntry::Category::STORAGE); // Table `database` // There are two reasons for having the origin column. // First, we can ensure that we don't have collisions in the origin hash we // use for the path because when we open the db we can make sure that the // origins exactly match. Second, chrome code crawling through the idb // directory can figure out the origin of every db without having to // reverse-engineer our hash scheme. nsresult rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE database" "( name TEXT PRIMARY KEY" ", origin TEXT NOT NULL" ", version INTEGER NOT NULL DEFAULT 0" ", last_vacuum_time INTEGER NOT NULL DEFAULT 0" ", last_analyze_time INTEGER NOT NULL DEFAULT 0" ", last_vacuum_size INTEGER NOT NULL DEFAULT 0" ") WITHOUT ROWID;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Table `object_store` rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE object_store" "( id INTEGER PRIMARY KEY" ", auto_increment INTEGER NOT NULL DEFAULT 0" ", name TEXT NOT NULL" ", key_path TEXT" ");" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Table `object_store_index` rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE object_store_index" "( id INTEGER PRIMARY KEY" ", object_store_id INTEGER NOT NULL" ", name TEXT NOT NULL" ", key_path TEXT NOT NULL" ", unique_index INTEGER NOT NULL" ", multientry INTEGER NOT NULL" ", locale TEXT" ", is_auto_locale BOOLEAN NOT NULL" ", FOREIGN KEY (object_store_id) " "REFERENCES object_store(id) " ");" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Table `object_data` rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE object_data" "( object_store_id INTEGER NOT NULL" ", key BLOB NOT NULL" ", index_data_values BLOB DEFAULT NULL" ", file_ids TEXT" ", data BLOB NOT NULL" ", PRIMARY KEY (object_store_id, key)" ", FOREIGN KEY (object_store_id) " "REFERENCES object_store(id) " ") WITHOUT ROWID;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Table `index_data` rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE index_data" "( index_id INTEGER NOT NULL" ", value BLOB NOT NULL" ", object_data_key BLOB NOT NULL" ", object_store_id INTEGER NOT NULL" ", value_locale BLOB" ", PRIMARY KEY (index_id, value, object_data_key)" ", FOREIGN KEY (index_id) " "REFERENCES object_store_index(id) " ", FOREIGN KEY (object_store_id, object_data_key) " "REFERENCES object_data(object_store_id, key) " ") WITHOUT ROWID;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE INDEX index_data_value_locale_index " "ON index_data (index_id, value_locale, object_data_key, value) " "WHERE value_locale IS NOT NULL;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Table `unique_index_data` rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE unique_index_data" "( index_id INTEGER NOT NULL" ", value BLOB NOT NULL" ", object_store_id INTEGER NOT NULL" ", object_data_key BLOB NOT NULL" ", value_locale BLOB" ", PRIMARY KEY (index_id, value)" ", FOREIGN KEY (index_id) " "REFERENCES object_store_index(id) " ", FOREIGN KEY (object_store_id, object_data_key) " "REFERENCES object_data(object_store_id, key) " ") WITHOUT ROWID;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE INDEX unique_index_data_value_locale_index " "ON unique_index_data (index_id, value_locale, object_data_key, value) " "WHERE value_locale IS NOT NULL;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = CreateFileTables(aConnection); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->SetSchemaVersion(kSQLiteSchemaVersion); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } nsresult UpgradeSchemaFrom4To5(mozIStorageConnection* aConnection) { AssertIsOnIOThread(); MOZ_ASSERT(aConnection); PROFILER_LABEL("IndexedDB", "UpgradeSchemaFrom4To5", js::ProfileEntry::Category::STORAGE); nsresult rv; // All we changed is the type of the version column, so lets try to // convert that to an integer, and if we fail, set it to 0. nsCOMPtr<mozIStorageStatement> stmt; rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( "SELECT name, version, dataVersion " "FROM database" ), getter_AddRefs(stmt)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsString name; int32_t intVersion; int64_t dataVersion; { mozStorageStatementScoper scoper(stmt); bool hasResults; rv = stmt->ExecuteStep(&hasResults); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (NS_WARN_IF(!hasResults)) { return NS_ERROR_FAILURE; } nsString version; rv = stmt->GetString(1, version); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } intVersion = version.ToInteger(&rv); if (NS_FAILED(rv)) { intVersion = 0; } rv = stmt->GetString(0, name); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->GetInt64(2, &dataVersion); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TABLE database" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE database (" "name TEXT NOT NULL, " "version INTEGER NOT NULL DEFAULT 0, " "dataVersion INTEGER NOT NULL" ");" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( "INSERT INTO database (name, version, dataVersion) " "VALUES (:name, :version, :dataVersion)" ), getter_AddRefs(stmt)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } { mozStorageStatementScoper scoper(stmt); rv = stmt->BindStringParameter(0, name); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->BindInt32Parameter(1, intVersion); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->BindInt64Parameter(2, dataVersion); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->Execute(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } rv = aConnection->SetSchemaVersion(5); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } nsresult UpgradeSchemaFrom5To6(mozIStorageConnection* aConnection) { AssertIsOnIOThread(); MOZ_ASSERT(aConnection); PROFILER_LABEL("IndexedDB", "UpgradeSchemaFrom5To6", js::ProfileEntry::Category::STORAGE); // First, drop all the indexes we're no longer going to use. nsresult rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP INDEX key_index;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP INDEX ai_key_index;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP INDEX value_index;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP INDEX ai_value_index;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Now, reorder the columns of object_data to put the blob data last. We do // this by copying into a temporary table, dropping the original, then copying // back into a newly created table. rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TEMPORARY TABLE temp_upgrade (" "id INTEGER PRIMARY KEY, " "object_store_id, " "key_value, " "data " ");" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "INSERT INTO temp_upgrade " "SELECT id, object_store_id, key_value, data " "FROM object_data;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TABLE object_data;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE object_data (" "id INTEGER PRIMARY KEY, " "object_store_id INTEGER NOT NULL, " "key_value DEFAULT NULL, " "data BLOB NOT NULL, " "UNIQUE (object_store_id, key_value), " "FOREIGN KEY (object_store_id) REFERENCES object_store(id) ON DELETE " "CASCADE" ");" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "INSERT INTO object_data " "SELECT id, object_store_id, key_value, data " "FROM temp_upgrade;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TABLE temp_upgrade;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // We need to add a unique constraint to our ai_object_data table. Copy all // the data out of it using a temporary table as before. rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TEMPORARY TABLE temp_upgrade (" "id INTEGER PRIMARY KEY, " "object_store_id, " "data " ");" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "INSERT INTO temp_upgrade " "SELECT id, object_store_id, data " "FROM ai_object_data;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TABLE ai_object_data;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE ai_object_data (" "id INTEGER PRIMARY KEY AUTOINCREMENT, " "object_store_id INTEGER NOT NULL, " "data BLOB NOT NULL, " "UNIQUE (object_store_id, id), " "FOREIGN KEY (object_store_id) REFERENCES object_store(id) ON DELETE " "CASCADE" ");" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "INSERT INTO ai_object_data " "SELECT id, object_store_id, data " "FROM temp_upgrade;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TABLE temp_upgrade;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Fix up the index_data table. We're reordering the columns as well as // changing the primary key from being a simple id to being a composite. rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TEMPORARY TABLE temp_upgrade (" "index_id, " "value, " "object_data_key, " "object_data_id " ");" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "INSERT INTO temp_upgrade " "SELECT index_id, value, object_data_key, object_data_id " "FROM index_data;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TABLE index_data;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE index_data (" "index_id INTEGER NOT NULL, " "value NOT NULL, " "object_data_key NOT NULL, " "object_data_id INTEGER NOT NULL, " "PRIMARY KEY (index_id, value, object_data_key), " "FOREIGN KEY (index_id) REFERENCES object_store_index(id) ON DELETE " "CASCADE, " "FOREIGN KEY (object_data_id) REFERENCES object_data(id) ON DELETE " "CASCADE" ");" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "INSERT OR IGNORE INTO index_data " "SELECT index_id, value, object_data_key, object_data_id " "FROM temp_upgrade;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TABLE temp_upgrade;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE INDEX index_data_object_data_id_index " "ON index_data (object_data_id);" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Fix up the unique_index_data table. We're reordering the columns as well as // changing the primary key from being a simple id to being a composite. rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TEMPORARY TABLE temp_upgrade (" "index_id, " "value, " "object_data_key, " "object_data_id " ");" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "INSERT INTO temp_upgrade " "SELECT index_id, value, object_data_key, object_data_id " "FROM unique_index_data;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TABLE unique_index_data;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE unique_index_data (" "index_id INTEGER NOT NULL, " "value NOT NULL, " "object_data_key NOT NULL, " "object_data_id INTEGER NOT NULL, " "PRIMARY KEY (index_id, value, object_data_key), " "UNIQUE (index_id, value), " "FOREIGN KEY (index_id) REFERENCES object_store_index(id) ON DELETE " "CASCADE " "FOREIGN KEY (object_data_id) REFERENCES object_data(id) ON DELETE " "CASCADE" ");" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "INSERT INTO unique_index_data " "SELECT index_id, value, object_data_key, object_data_id " "FROM temp_upgrade;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TABLE temp_upgrade;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE INDEX unique_index_data_object_data_id_index " "ON unique_index_data (object_data_id);" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Fix up the ai_index_data table. We're reordering the columns as well as // changing the primary key from being a simple id to being a composite. rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TEMPORARY TABLE temp_upgrade (" "index_id, " "value, " "ai_object_data_id " ");" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "INSERT INTO temp_upgrade " "SELECT index_id, value, ai_object_data_id " "FROM ai_index_data;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TABLE ai_index_data;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE ai_index_data (" "index_id INTEGER NOT NULL, " "value NOT NULL, " "ai_object_data_id INTEGER NOT NULL, " "PRIMARY KEY (index_id, value, ai_object_data_id), " "FOREIGN KEY (index_id) REFERENCES object_store_index(id) ON DELETE " "CASCADE, " "FOREIGN KEY (ai_object_data_id) REFERENCES ai_object_data(id) ON DELETE " "CASCADE" ");" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "INSERT OR IGNORE INTO ai_index_data " "SELECT index_id, value, ai_object_data_id " "FROM temp_upgrade;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TABLE temp_upgrade;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE INDEX ai_index_data_ai_object_data_id_index " "ON ai_index_data (ai_object_data_id);" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Fix up the ai_unique_index_data table. We're reordering the columns as well // as changing the primary key from being a simple id to being a composite. rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TEMPORARY TABLE temp_upgrade (" "index_id, " "value, " "ai_object_data_id " ");" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "INSERT INTO temp_upgrade " "SELECT index_id, value, ai_object_data_id " "FROM ai_unique_index_data;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TABLE ai_unique_index_data;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE ai_unique_index_data (" "index_id INTEGER NOT NULL, " "value NOT NULL, " "ai_object_data_id INTEGER NOT NULL, " "UNIQUE (index_id, value), " "PRIMARY KEY (index_id, value, ai_object_data_id), " "FOREIGN KEY (index_id) REFERENCES object_store_index(id) ON DELETE " "CASCADE, " "FOREIGN KEY (ai_object_data_id) REFERENCES ai_object_data(id) ON DELETE " "CASCADE" ");" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "INSERT INTO ai_unique_index_data " "SELECT index_id, value, ai_object_data_id " "FROM temp_upgrade;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TABLE temp_upgrade;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE INDEX ai_unique_index_data_ai_object_data_id_index " "ON ai_unique_index_data (ai_object_data_id);" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->SetSchemaVersion(6); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } nsresult UpgradeSchemaFrom6To7(mozIStorageConnection* aConnection) { AssertIsOnIOThread(); MOZ_ASSERT(aConnection); PROFILER_LABEL("IndexedDB", "UpgradeSchemaFrom6To7", js::ProfileEntry::Category::STORAGE); nsresult rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TEMPORARY TABLE temp_upgrade (" "id, " "name, " "key_path, " "auto_increment" ");" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "INSERT INTO temp_upgrade " "SELECT id, name, key_path, auto_increment " "FROM object_store;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TABLE object_store;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE object_store (" "id INTEGER PRIMARY KEY, " "auto_increment INTEGER NOT NULL DEFAULT 0, " "name TEXT NOT NULL, " "key_path TEXT, " "UNIQUE (name)" ");" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "INSERT INTO object_store " "SELECT id, auto_increment, name, nullif(key_path, '') " "FROM temp_upgrade;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TABLE temp_upgrade;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->SetSchemaVersion(7); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } nsresult UpgradeSchemaFrom7To8(mozIStorageConnection* aConnection) { AssertIsOnIOThread(); MOZ_ASSERT(aConnection); PROFILER_LABEL("IndexedDB", "UpgradeSchemaFrom7To8", js::ProfileEntry::Category::STORAGE); nsresult rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TEMPORARY TABLE temp_upgrade (" "id, " "object_store_id, " "name, " "key_path, " "unique_index, " "object_store_autoincrement" ");" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "INSERT INTO temp_upgrade " "SELECT id, object_store_id, name, key_path, " "unique_index, object_store_autoincrement " "FROM object_store_index;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TABLE object_store_index;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE object_store_index (" "id INTEGER, " "object_store_id INTEGER NOT NULL, " "name TEXT NOT NULL, " "key_path TEXT NOT NULL, " "unique_index INTEGER NOT NULL, " "multientry INTEGER NOT NULL, " "object_store_autoincrement INTERGER NOT NULL, " "PRIMARY KEY (id), " "UNIQUE (object_store_id, name), " "FOREIGN KEY (object_store_id) REFERENCES object_store(id) ON DELETE " "CASCADE" ");" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "INSERT INTO object_store_index " "SELECT id, object_store_id, name, key_path, " "unique_index, 0, object_store_autoincrement " "FROM temp_upgrade;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TABLE temp_upgrade;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->SetSchemaVersion(8); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } class CompressDataBlobsFunction final : public mozIStorageFunction { public: NS_DECL_ISUPPORTS private: ~CompressDataBlobsFunction() { } NS_IMETHOD OnFunctionCall(mozIStorageValueArray* aArguments, nsIVariant** aResult) override { MOZ_ASSERT(aArguments); MOZ_ASSERT(aResult); PROFILER_LABEL("IndexedDB", "CompressDataBlobsFunction::OnFunctionCall", js::ProfileEntry::Category::STORAGE); uint32_t argc; nsresult rv = aArguments->GetNumEntries(&argc); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (argc != 1) { NS_WARNING("Don't call me with the wrong number of arguments!"); return NS_ERROR_UNEXPECTED; } int32_t type; rv = aArguments->GetTypeOfIndex(0, &type); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (type != mozIStorageStatement::VALUE_TYPE_BLOB) { NS_WARNING("Don't call me with the wrong type of arguments!"); return NS_ERROR_UNEXPECTED; } const uint8_t* uncompressed; uint32_t uncompressedLength; rv = aArguments->GetSharedBlob(0, &uncompressedLength, &uncompressed); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } size_t compressedLength = snappy::MaxCompressedLength(uncompressedLength); UniqueFreePtr<uint8_t> compressed( static_cast<uint8_t*>(malloc(compressedLength))); if (NS_WARN_IF(!compressed)) { return NS_ERROR_OUT_OF_MEMORY; } snappy::RawCompress(reinterpret_cast<const char*>(uncompressed), uncompressedLength, reinterpret_cast<char*>(compressed.get()), &compressedLength); std::pair<uint8_t *, int> data(compressed.release(), int(compressedLength)); nsCOMPtr<nsIVariant> result = new mozilla::storage::AdoptedBlobVariant(data); result.forget(aResult); return NS_OK; } }; nsresult UpgradeSchemaFrom8To9_0(mozIStorageConnection* aConnection) { AssertIsOnIOThread(); MOZ_ASSERT(aConnection); PROFILER_LABEL("IndexedDB", "UpgradeSchemaFrom8To9_0", js::ProfileEntry::Category::STORAGE); // We no longer use the dataVersion column. nsresult rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "UPDATE database SET dataVersion = 0;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsCOMPtr<mozIStorageFunction> compressor = new CompressDataBlobsFunction(); NS_NAMED_LITERAL_CSTRING(compressorName, "compress"); rv = aConnection->CreateFunction(compressorName, 1, compressor); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Turn off foreign key constraints before we do anything here. rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "UPDATE object_data SET data = compress(data);" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "UPDATE ai_object_data SET data = compress(data);" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->RemoveFunction(compressorName); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->SetSchemaVersion(MakeSchemaVersion(9, 0)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } nsresult UpgradeSchemaFrom9_0To10_0(mozIStorageConnection* aConnection) { AssertIsOnIOThread(); MOZ_ASSERT(aConnection); PROFILER_LABEL("IndexedDB", "UpgradeSchemaFrom9_0To10_0", js::ProfileEntry::Category::STORAGE); nsresult rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "ALTER TABLE object_data ADD COLUMN file_ids TEXT;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "ALTER TABLE ai_object_data ADD COLUMN file_ids TEXT;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = CreateFileTables(aConnection); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->SetSchemaVersion(MakeSchemaVersion(10, 0)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } nsresult UpgradeSchemaFrom10_0To11_0(mozIStorageConnection* aConnection) { AssertIsOnIOThread(); MOZ_ASSERT(aConnection); PROFILER_LABEL("IndexedDB", "UpgradeSchemaFrom10_0To11_0", js::ProfileEntry::Category::STORAGE); nsresult rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TEMPORARY TABLE temp_upgrade (" "id, " "object_store_id, " "name, " "key_path, " "unique_index, " "multientry" ");" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "INSERT INTO temp_upgrade " "SELECT id, object_store_id, name, key_path, " "unique_index, multientry " "FROM object_store_index;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TABLE object_store_index;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE object_store_index (" "id INTEGER PRIMARY KEY, " "object_store_id INTEGER NOT NULL, " "name TEXT NOT NULL, " "key_path TEXT NOT NULL, " "unique_index INTEGER NOT NULL, " "multientry INTEGER NOT NULL, " "UNIQUE (object_store_id, name), " "FOREIGN KEY (object_store_id) REFERENCES object_store(id) ON DELETE " "CASCADE" ");" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "INSERT INTO object_store_index " "SELECT id, object_store_id, name, key_path, " "unique_index, multientry " "FROM temp_upgrade;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TABLE temp_upgrade;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TRIGGER object_data_insert_trigger;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "INSERT INTO object_data (object_store_id, key_value, data, file_ids) " "SELECT object_store_id, id, data, file_ids " "FROM ai_object_data;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TRIGGER object_data_insert_trigger " "AFTER INSERT ON object_data " "FOR EACH ROW " "WHEN NEW.file_ids IS NOT NULL " "BEGIN " "SELECT update_refcount(NULL, NEW.file_ids); " "END;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "INSERT INTO index_data (index_id, value, object_data_key, object_data_id) " "SELECT ai_index_data.index_id, ai_index_data.value, ai_index_data.ai_object_data_id, object_data.id " "FROM ai_index_data " "INNER JOIN object_store_index ON " "object_store_index.id = ai_index_data.index_id " "INNER JOIN object_data ON " "object_data.object_store_id = object_store_index.object_store_id AND " "object_data.key_value = ai_index_data.ai_object_data_id;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "INSERT INTO unique_index_data (index_id, value, object_data_key, object_data_id) " "SELECT ai_unique_index_data.index_id, ai_unique_index_data.value, ai_unique_index_data.ai_object_data_id, object_data.id " "FROM ai_unique_index_data " "INNER JOIN object_store_index ON " "object_store_index.id = ai_unique_index_data.index_id " "INNER JOIN object_data ON " "object_data.object_store_id = object_store_index.object_store_id AND " "object_data.key_value = ai_unique_index_data.ai_object_data_id;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "UPDATE object_store " "SET auto_increment = (SELECT max(id) FROM ai_object_data) + 1 " "WHERE auto_increment;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TABLE ai_unique_index_data;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TABLE ai_index_data;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TABLE ai_object_data;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->SetSchemaVersion(MakeSchemaVersion(11, 0)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } class EncodeKeysFunction final : public mozIStorageFunction { public: NS_DECL_ISUPPORTS private: ~EncodeKeysFunction() { } NS_IMETHOD OnFunctionCall(mozIStorageValueArray* aArguments, nsIVariant** aResult) override { MOZ_ASSERT(aArguments); MOZ_ASSERT(aResult); PROFILER_LABEL("IndexedDB", "EncodeKeysFunction::OnFunctionCall", js::ProfileEntry::Category::STORAGE); uint32_t argc; nsresult rv = aArguments->GetNumEntries(&argc); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (argc != 1) { NS_WARNING("Don't call me with the wrong number of arguments!"); return NS_ERROR_UNEXPECTED; } int32_t type; rv = aArguments->GetTypeOfIndex(0, &type); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } Key key; if (type == mozIStorageStatement::VALUE_TYPE_INTEGER) { int64_t intKey; aArguments->GetInt64(0, &intKey); key.SetFromInteger(intKey); } else if (type == mozIStorageStatement::VALUE_TYPE_TEXT) { nsString stringKey; aArguments->GetString(0, stringKey); key.SetFromString(stringKey); } else { NS_WARNING("Don't call me with the wrong type of arguments!"); return NS_ERROR_UNEXPECTED; } const nsCString& buffer = key.GetBuffer(); std::pair<const void *, int> data(static_cast<const void*>(buffer.get()), int(buffer.Length())); nsCOMPtr<nsIVariant> result = new mozilla::storage::BlobVariant(data); result.forget(aResult); return NS_OK; } }; nsresult UpgradeSchemaFrom11_0To12_0(mozIStorageConnection* aConnection) { AssertIsOnIOThread(); MOZ_ASSERT(aConnection); PROFILER_LABEL("IndexedDB", "UpgradeSchemaFrom11_0To12_0", js::ProfileEntry::Category::STORAGE); NS_NAMED_LITERAL_CSTRING(encoderName, "encode"); nsCOMPtr<mozIStorageFunction> encoder = new EncodeKeysFunction(); nsresult rv = aConnection->CreateFunction(encoderName, 1, encoder); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TEMPORARY TABLE temp_upgrade (" "id INTEGER PRIMARY KEY, " "object_store_id, " "key_value, " "data, " "file_ids " ");" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "INSERT INTO temp_upgrade " "SELECT id, object_store_id, encode(key_value), data, file_ids " "FROM object_data;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TABLE object_data;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE object_data (" "id INTEGER PRIMARY KEY, " "object_store_id INTEGER NOT NULL, " "key_value BLOB DEFAULT NULL, " "file_ids TEXT, " "data BLOB NOT NULL, " "UNIQUE (object_store_id, key_value), " "FOREIGN KEY (object_store_id) REFERENCES object_store(id) ON DELETE " "CASCADE" ");" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "INSERT INTO object_data " "SELECT id, object_store_id, key_value, file_ids, data " "FROM temp_upgrade;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TABLE temp_upgrade;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TRIGGER object_data_insert_trigger " "AFTER INSERT ON object_data " "FOR EACH ROW " "WHEN NEW.file_ids IS NOT NULL " "BEGIN " "SELECT update_refcount(NULL, NEW.file_ids); " "END;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TRIGGER object_data_update_trigger " "AFTER UPDATE OF file_ids ON object_data " "FOR EACH ROW " "WHEN OLD.file_ids IS NOT NULL OR NEW.file_ids IS NOT NULL " "BEGIN " "SELECT update_refcount(OLD.file_ids, NEW.file_ids); " "END;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TRIGGER object_data_delete_trigger " "AFTER DELETE ON object_data " "FOR EACH ROW WHEN OLD.file_ids IS NOT NULL " "BEGIN " "SELECT update_refcount(OLD.file_ids, NULL); " "END;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TEMPORARY TABLE temp_upgrade (" "index_id, " "value, " "object_data_key, " "object_data_id " ");" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "INSERT INTO temp_upgrade " "SELECT index_id, encode(value), encode(object_data_key), object_data_id " "FROM index_data;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TABLE index_data;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE index_data (" "index_id INTEGER NOT NULL, " "value BLOB NOT NULL, " "object_data_key BLOB NOT NULL, " "object_data_id INTEGER NOT NULL, " "PRIMARY KEY (index_id, value, object_data_key), " "FOREIGN KEY (index_id) REFERENCES object_store_index(id) ON DELETE " "CASCADE, " "FOREIGN KEY (object_data_id) REFERENCES object_data(id) ON DELETE " "CASCADE" ");" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "INSERT INTO index_data " "SELECT index_id, value, object_data_key, object_data_id " "FROM temp_upgrade;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TABLE temp_upgrade;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE INDEX index_data_object_data_id_index " "ON index_data (object_data_id);" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TEMPORARY TABLE temp_upgrade (" "index_id, " "value, " "object_data_key, " "object_data_id " ");" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "INSERT INTO temp_upgrade " "SELECT index_id, encode(value), encode(object_data_key), object_data_id " "FROM unique_index_data;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TABLE unique_index_data;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE unique_index_data (" "index_id INTEGER NOT NULL, " "value BLOB NOT NULL, " "object_data_key BLOB NOT NULL, " "object_data_id INTEGER NOT NULL, " "PRIMARY KEY (index_id, value, object_data_key), " "UNIQUE (index_id, value), " "FOREIGN KEY (index_id) REFERENCES object_store_index(id) ON DELETE " "CASCADE " "FOREIGN KEY (object_data_id) REFERENCES object_data(id) ON DELETE " "CASCADE" ");" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "INSERT INTO unique_index_data " "SELECT index_id, value, object_data_key, object_data_id " "FROM temp_upgrade;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TABLE temp_upgrade;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE INDEX unique_index_data_object_data_id_index " "ON unique_index_data (object_data_id);" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->RemoveFunction(encoderName); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->SetSchemaVersion(MakeSchemaVersion(12, 0)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } nsresult UpgradeSchemaFrom12_0To13_0(mozIStorageConnection* aConnection, bool* aVacuumNeeded) { AssertIsOnIOThread(); MOZ_ASSERT(aConnection); PROFILER_LABEL("IndexedDB", "UpgradeSchemaFrom12_0To13_0", js::ProfileEntry::Category::STORAGE); nsresult rv; #ifdef IDB_MOBILE int32_t defaultPageSize; rv = aConnection->GetDefaultPageSize(&defaultPageSize); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Enable auto_vacuum mode and update the page size to the platform default. nsAutoCString upgradeQuery("PRAGMA auto_vacuum = FULL; PRAGMA page_size = "); upgradeQuery.AppendInt(defaultPageSize); rv = aConnection->ExecuteSimpleSQL(upgradeQuery); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } *aVacuumNeeded = true; #endif rv = aConnection->SetSchemaVersion(MakeSchemaVersion(13, 0)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } nsresult UpgradeSchemaFrom13_0To14_0(mozIStorageConnection* aConnection) { AssertIsOnIOThread(); MOZ_ASSERT(aConnection); // The only change between 13 and 14 was a different structured // clone format, but it's backwards-compatible. nsresult rv = aConnection->SetSchemaVersion(MakeSchemaVersion(14, 0)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } nsresult UpgradeSchemaFrom14_0To15_0(mozIStorageConnection* aConnection) { // The only change between 14 and 15 was a different structured // clone format, but it's backwards-compatible. nsresult rv = aConnection->SetSchemaVersion(MakeSchemaVersion(15, 0)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } nsresult UpgradeSchemaFrom15_0To16_0(mozIStorageConnection* aConnection) { // The only change between 15 and 16 was a different structured // clone format, but it's backwards-compatible. nsresult rv = aConnection->SetSchemaVersion(MakeSchemaVersion(16, 0)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } nsresult UpgradeSchemaFrom16_0To17_0(mozIStorageConnection* aConnection) { // The only change between 16 and 17 was a different structured // clone format, but it's backwards-compatible. nsresult rv = aConnection->SetSchemaVersion(MakeSchemaVersion(17, 0)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } class UpgradeSchemaFrom17_0To18_0Helper final { class InsertIndexDataValuesFunction; class UpgradeKeyFunction; public: static nsresult DoUpgrade(mozIStorageConnection* aConnection, const nsACString& aOrigin); private: static nsresult DoUpgradeInternal(mozIStorageConnection* aConnection, const nsACString& aOrigin); UpgradeSchemaFrom17_0To18_0Helper() { MOZ_ASSERT_UNREACHABLE("Don't create instances of this class!"); } ~UpgradeSchemaFrom17_0To18_0Helper() { MOZ_ASSERT_UNREACHABLE("Don't create instances of this class!"); } }; class UpgradeSchemaFrom17_0To18_0Helper::InsertIndexDataValuesFunction final : public mozIStorageFunction { public: InsertIndexDataValuesFunction() { } NS_DECL_ISUPPORTS private: ~InsertIndexDataValuesFunction() { } NS_DECL_MOZISTORAGEFUNCTION }; NS_IMPL_ISUPPORTS(UpgradeSchemaFrom17_0To18_0Helper:: InsertIndexDataValuesFunction, mozIStorageFunction); NS_IMETHODIMP UpgradeSchemaFrom17_0To18_0Helper:: InsertIndexDataValuesFunction::OnFunctionCall(mozIStorageValueArray* aValues, nsIVariant** _retval) { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(aValues); MOZ_ASSERT(_retval); #ifdef DEBUG { uint32_t argCount; MOZ_ALWAYS_SUCCEEDS(aValues->GetNumEntries(&argCount)); MOZ_ASSERT(argCount == 4); int32_t valueType; MOZ_ALWAYS_SUCCEEDS(aValues->GetTypeOfIndex(0, &valueType)); MOZ_ASSERT(valueType == mozIStorageValueArray::VALUE_TYPE_NULL || valueType == mozIStorageValueArray::VALUE_TYPE_BLOB); MOZ_ALWAYS_SUCCEEDS(aValues->GetTypeOfIndex(1, &valueType)); MOZ_ASSERT(valueType == mozIStorageValueArray::VALUE_TYPE_INTEGER); MOZ_ALWAYS_SUCCEEDS(aValues->GetTypeOfIndex(2, &valueType)); MOZ_ASSERT(valueType == mozIStorageValueArray::VALUE_TYPE_INTEGER); MOZ_ALWAYS_SUCCEEDS(aValues->GetTypeOfIndex(3, &valueType)); MOZ_ASSERT(valueType == mozIStorageValueArray::VALUE_TYPE_BLOB); } #endif // Read out the previous value. It may be NULL, in which case we'll just end // up with an empty array. AutoTArray<IndexDataValue, 32> indexValues; nsresult rv = ReadCompressedIndexDataValues(aValues, 0, indexValues); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } int64_t indexId; rv = aValues->GetInt64(1, &indexId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } int32_t unique; rv = aValues->GetInt32(2, &unique); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } Key value; rv = value.SetFromValueArray(aValues, 3); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Update the array with the new addition. if (NS_WARN_IF(!indexValues.SetCapacity(indexValues.Length() + 1, fallible))) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_OUT_OF_MEMORY; } MOZ_ALWAYS_TRUE( indexValues.InsertElementSorted(IndexDataValue(indexId, !!unique, value), fallible)); // Compress the array. UniqueFreePtr<uint8_t> indexValuesBlob; uint32_t indexValuesBlobLength; rv = MakeCompressedIndexDataValues(indexValues, indexValuesBlob, &indexValuesBlobLength); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // The compressed blob is the result of this function. std::pair<uint8_t *, int> indexValuesBlobPair(indexValuesBlob.release(), indexValuesBlobLength); nsCOMPtr<nsIVariant> result = new storage::AdoptedBlobVariant(indexValuesBlobPair); result.forget(_retval); return NS_OK; } class UpgradeSchemaFrom17_0To18_0Helper::UpgradeKeyFunction final : public mozIStorageFunction { public: UpgradeKeyFunction() { } static nsresult CopyAndUpgradeKeyBuffer(const uint8_t* aSource, const uint8_t* aSourceEnd, uint8_t* aDestination) { return CopyAndUpgradeKeyBufferInternal(aSource, aSourceEnd, aDestination, 0 /* aTagOffset */, 0 /* aRecursionDepth */); } NS_DECL_ISUPPORTS private: ~UpgradeKeyFunction() { } static nsresult CopyAndUpgradeKeyBufferInternal(const uint8_t*& aSource, const uint8_t* aSourceEnd, uint8_t*& aDestination, uint8_t aTagOffset, uint8_t aRecursionDepth); static uint32_t AdjustedSize(uint32_t aMaxSize, const uint8_t* aSource, const uint8_t* aSourceEnd) { MOZ_ASSERT(aMaxSize); MOZ_ASSERT(aSource); MOZ_ASSERT(aSourceEnd); MOZ_ASSERT(aSource <= aSourceEnd); return std::min(aMaxSize, uint32_t(aSourceEnd - aSource)); } NS_DECL_MOZISTORAGEFUNCTION }; // static nsresult UpgradeSchemaFrom17_0To18_0Helper:: UpgradeKeyFunction::CopyAndUpgradeKeyBufferInternal(const uint8_t*& aSource, const uint8_t* aSourceEnd, uint8_t*& aDestination, uint8_t aTagOffset, uint8_t aRecursionDepth) { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(aSource); MOZ_ASSERT(*aSource); MOZ_ASSERT(aSourceEnd); MOZ_ASSERT(aSource < aSourceEnd); MOZ_ASSERT(aDestination); MOZ_ASSERT(aTagOffset <= Key::kMaxArrayCollapse); static constexpr uint8_t kOldNumberTag = 0x1; static constexpr uint8_t kOldDateTag = 0x2; static constexpr uint8_t kOldStringTag = 0x3; static constexpr uint8_t kOldArrayTag = 0x4; static constexpr uint8_t kOldMaxType = kOldArrayTag; if (NS_WARN_IF(aRecursionDepth > Key::kMaxRecursionDepth)) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_FILE_CORRUPTED; } const uint8_t sourceTag = *aSource - (aTagOffset * kOldMaxType); MOZ_ASSERT(sourceTag); if (NS_WARN_IF(sourceTag > kOldMaxType * Key::kMaxArrayCollapse)) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_FILE_CORRUPTED; } if (sourceTag == kOldNumberTag || sourceTag == kOldDateTag) { // Write the new tag. *aDestination++ = (sourceTag == kOldNumberTag ? Key::eFloat : Key::eDate) + (aTagOffset * Key::eMaxType); aSource++; // Numbers and Dates are encoded as 64-bit integers, but trailing 0 // bytes have been removed. const uint32_t byteCount = AdjustedSize(sizeof(uint64_t), aSource, aSourceEnd); for (uint32_t count = 0; count < byteCount; count++) { *aDestination++ = *aSource++; } return NS_OK; } if (sourceTag == kOldStringTag) { // Write the new tag. *aDestination++ = Key::eString + (aTagOffset * Key::eMaxType); aSource++; while (aSource < aSourceEnd) { const uint8_t byte = *aSource++; *aDestination++ = byte; if (!byte) { // Just copied the terminator. break; } // Maybe copy one or two extra bytes if the byte is tagged and we have // enough source space. if (byte & 0x80) { const uint32_t byteCount = AdjustedSize((byte & 0x40) ? 2 : 1, aSource, aSourceEnd); for (uint32_t count = 0; count < byteCount; count++) { *aDestination++ = *aSource++; } } } return NS_OK; } if (NS_WARN_IF(sourceTag < kOldArrayTag)) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_FILE_CORRUPTED; } aTagOffset++; if (aTagOffset == Key::kMaxArrayCollapse) { MOZ_ASSERT(sourceTag == kOldArrayTag); *aDestination++ = (aTagOffset * Key::eMaxType); aSource++; aTagOffset = 0; } while (aSource < aSourceEnd && (*aSource - (aTagOffset * kOldMaxType)) != Key::eTerminator) { nsresult rv = CopyAndUpgradeKeyBufferInternal(aSource, aSourceEnd, aDestination, aTagOffset, aRecursionDepth + 1); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } aTagOffset = 0; } if (aSource < aSourceEnd) { MOZ_ASSERT((*aSource - (aTagOffset * kOldMaxType)) == Key::eTerminator); *aDestination++ = Key::eTerminator + (aTagOffset * Key::eMaxType); aSource++; } return NS_OK; } NS_IMPL_ISUPPORTS(UpgradeSchemaFrom17_0To18_0Helper::UpgradeKeyFunction, mozIStorageFunction); NS_IMETHODIMP UpgradeSchemaFrom17_0To18_0Helper:: UpgradeKeyFunction::OnFunctionCall(mozIStorageValueArray* aValues, nsIVariant** _retval) { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(aValues); MOZ_ASSERT(_retval); #ifdef DEBUG { uint32_t argCount; MOZ_ALWAYS_SUCCEEDS(aValues->GetNumEntries(&argCount)); MOZ_ASSERT(argCount == 1); int32_t valueType; MOZ_ALWAYS_SUCCEEDS(aValues->GetTypeOfIndex(0, &valueType)); MOZ_ASSERT(valueType == mozIStorageValueArray::VALUE_TYPE_BLOB); } #endif // Dig the old key out of the values. const uint8_t* blobData; uint32_t blobDataLength; nsresult rv = aValues->GetSharedBlob(0, &blobDataLength, &blobData); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Upgrading the key doesn't change the amount of space needed to hold it. UniqueFreePtr<uint8_t> upgradedBlobData( static_cast<uint8_t*>(malloc(blobDataLength))); if (NS_WARN_IF(!upgradedBlobData)) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_OUT_OF_MEMORY; } rv = CopyAndUpgradeKeyBuffer(blobData, blobData + blobDataLength, upgradedBlobData.get()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // The upgraded key is the result of this function. std::pair<uint8_t*, int> data(upgradedBlobData.release(), int(blobDataLength)); nsCOMPtr<nsIVariant> result = new mozilla::storage::AdoptedBlobVariant(data); result.forget(_retval); return NS_OK; } // static nsresult UpgradeSchemaFrom17_0To18_0Helper::DoUpgrade(mozIStorageConnection* aConnection, const nsACString& aOrigin) { MOZ_ASSERT(aConnection); MOZ_ASSERT(!aOrigin.IsEmpty()); // Register the |upgrade_key| function. RefPtr<UpgradeKeyFunction> updateFunction = new UpgradeKeyFunction(); NS_NAMED_LITERAL_CSTRING(upgradeKeyFunctionName, "upgrade_key"); nsresult rv = aConnection->CreateFunction(upgradeKeyFunctionName, 1, updateFunction); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Register the |insert_idv| function. RefPtr<InsertIndexDataValuesFunction> insertIDVFunction = new InsertIndexDataValuesFunction(); NS_NAMED_LITERAL_CSTRING(insertIDVFunctionName, "insert_idv"); rv = aConnection->CreateFunction(insertIDVFunctionName, 4, insertIDVFunction); if (NS_WARN_IF(NS_FAILED(rv))) { MOZ_ALWAYS_SUCCEEDS(aConnection->RemoveFunction(upgradeKeyFunctionName)); return rv; } rv = DoUpgradeInternal(aConnection, aOrigin); MOZ_ALWAYS_SUCCEEDS(aConnection->RemoveFunction(upgradeKeyFunctionName)); MOZ_ALWAYS_SUCCEEDS(aConnection->RemoveFunction(insertIDVFunctionName)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } // static nsresult UpgradeSchemaFrom17_0To18_0Helper::DoUpgradeInternal( mozIStorageConnection* aConnection, const nsACString& aOrigin) { MOZ_ASSERT(aConnection); MOZ_ASSERT(!aOrigin.IsEmpty()); nsresult rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( // Drop these triggers to avoid unnecessary work during the upgrade process. "DROP TRIGGER object_data_insert_trigger;" "DROP TRIGGER object_data_update_trigger;" "DROP TRIGGER object_data_delete_trigger;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( // Drop these indexes before we do anything else to free disk space. "DROP INDEX index_data_object_data_id_index;" "DROP INDEX unique_index_data_object_data_id_index;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Create the new tables and triggers first. rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( // This will eventually become the |database| table. "CREATE TABLE database_upgrade " "( name TEXT PRIMARY KEY" ", origin TEXT NOT NULL" ", version INTEGER NOT NULL DEFAULT 0" ", last_vacuum_time INTEGER NOT NULL DEFAULT 0" ", last_analyze_time INTEGER NOT NULL DEFAULT 0" ", last_vacuum_size INTEGER NOT NULL DEFAULT 0" ") WITHOUT ROWID;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( // This will eventually become the |object_store| table. "CREATE TABLE object_store_upgrade" "( id INTEGER PRIMARY KEY" ", auto_increment INTEGER NOT NULL DEFAULT 0" ", name TEXT NOT NULL" ", key_path TEXT" ");" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( // This will eventually become the |object_store_index| table. "CREATE TABLE object_store_index_upgrade" "( id INTEGER PRIMARY KEY" ", object_store_id INTEGER NOT NULL" ", name TEXT NOT NULL" ", key_path TEXT NOT NULL" ", unique_index INTEGER NOT NULL" ", multientry INTEGER NOT NULL" ", FOREIGN KEY (object_store_id) " "REFERENCES object_store(id) " ");" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( // This will eventually become the |object_data| table. "CREATE TABLE object_data_upgrade" "( object_store_id INTEGER NOT NULL" ", key BLOB NOT NULL" ", index_data_values BLOB DEFAULT NULL" ", file_ids TEXT" ", data BLOB NOT NULL" ", PRIMARY KEY (object_store_id, key)" ", FOREIGN KEY (object_store_id) " "REFERENCES object_store(id) " ") WITHOUT ROWID;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( // This will eventually become the |index_data| table. "CREATE TABLE index_data_upgrade" "( index_id INTEGER NOT NULL" ", value BLOB NOT NULL" ", object_data_key BLOB NOT NULL" ", object_store_id INTEGER NOT NULL" ", PRIMARY KEY (index_id, value, object_data_key)" ", FOREIGN KEY (index_id) " "REFERENCES object_store_index(id) " ", FOREIGN KEY (object_store_id, object_data_key) " "REFERENCES object_data(object_store_id, key) " ") WITHOUT ROWID;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( // This will eventually become the |unique_index_data| table. "CREATE TABLE unique_index_data_upgrade" "( index_id INTEGER NOT NULL" ", value BLOB NOT NULL" ", object_store_id INTEGER NOT NULL" ", object_data_key BLOB NOT NULL" ", PRIMARY KEY (index_id, value)" ", FOREIGN KEY (index_id) " "REFERENCES object_store_index(id) " ", FOREIGN KEY (object_store_id, object_data_key) " "REFERENCES object_data(object_store_id, key) " ") WITHOUT ROWID;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( // Temporarily store |index_data_values| that we build during the upgrade of // the index tables. We will later move this to the |object_data| table. "CREATE TEMPORARY TABLE temp_index_data_values " "( object_store_id INTEGER NOT NULL" ", key BLOB NOT NULL" ", index_data_values BLOB DEFAULT NULL" ", PRIMARY KEY (object_store_id, key)" ") WITHOUT ROWID;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( // These two triggers help build the |index_data_values| blobs. The nested // SELECT statements help us achieve an "INSERT OR UPDATE"-like behavior. "CREATE TEMPORARY TRIGGER unique_index_data_upgrade_insert_trigger " "AFTER INSERT ON unique_index_data_upgrade " "BEGIN " "INSERT OR REPLACE INTO temp_index_data_values " "VALUES " "( NEW.object_store_id" ", NEW.object_data_key" ", insert_idv(" "( SELECT index_data_values " "FROM temp_index_data_values " "WHERE object_store_id = NEW.object_store_id " "AND key = NEW.object_data_key " "), NEW.index_id" ", 1" /* unique */ ", NEW.value" ")" ");" "END;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TEMPORARY TRIGGER index_data_upgrade_insert_trigger " "AFTER INSERT ON index_data_upgrade " "BEGIN " "INSERT OR REPLACE INTO temp_index_data_values " "VALUES " "( NEW.object_store_id" ", NEW.object_data_key" ", insert_idv(" "(" "SELECT index_data_values " "FROM temp_index_data_values " "WHERE object_store_id = NEW.object_store_id " "AND key = NEW.object_data_key " "), NEW.index_id" ", 0" /* not unique */ ", NEW.value" ")" ");" "END;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Update the |unique_index_data| table to change the column order, remove the // ON DELETE CASCADE clauses, and to apply the WITHOUT ROWID optimization. rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( // Insert all the data. "INSERT INTO unique_index_data_upgrade " "SELECT " "unique_index_data.index_id, " "upgrade_key(unique_index_data.value), " "object_data.object_store_id, " "upgrade_key(unique_index_data.object_data_key) " "FROM unique_index_data " "JOIN object_data " "ON unique_index_data.object_data_id = object_data.id;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( // The trigger is no longer needed. "DROP TRIGGER unique_index_data_upgrade_insert_trigger;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( // The old table is no longer needed. "DROP TABLE unique_index_data;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( // Rename the table. "ALTER TABLE unique_index_data_upgrade " "RENAME TO unique_index_data;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Update the |index_data| table to change the column order, remove the ON // DELETE CASCADE clauses, and to apply the WITHOUT ROWID optimization. rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( // Insert all the data. "INSERT INTO index_data_upgrade " "SELECT " "index_data.index_id, " "upgrade_key(index_data.value), " "upgrade_key(index_data.object_data_key), " "object_data.object_store_id " "FROM index_data " "JOIN object_data " "ON index_data.object_data_id = object_data.id;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( // The trigger is no longer needed. "DROP TRIGGER index_data_upgrade_insert_trigger;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( // The old table is no longer needed. "DROP TABLE index_data;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( // Rename the table. "ALTER TABLE index_data_upgrade " "RENAME TO index_data;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Update the |object_data| table to add the |index_data_values| column, // remove the ON DELETE CASCADE clause, and apply the WITHOUT ROWID // optimization. rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( // Insert all the data. "INSERT INTO object_data_upgrade " "SELECT " "object_data.object_store_id, " "upgrade_key(object_data.key_value), " "temp_index_data_values.index_data_values, " "object_data.file_ids, " "object_data.data " "FROM object_data " "LEFT JOIN temp_index_data_values " "ON object_data.object_store_id = " "temp_index_data_values.object_store_id " "AND upgrade_key(object_data.key_value) = " "temp_index_data_values.key;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( // The temporary table is no longer needed. "DROP TABLE temp_index_data_values;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( // The old table is no longer needed. "DROP TABLE object_data;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( // Rename the table. "ALTER TABLE object_data_upgrade " "RENAME TO object_data;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Update the |object_store_index| table to remove the UNIQUE constraint and // the ON DELETE CASCADE clause. rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "INSERT INTO object_store_index_upgrade " "SELECT * " "FROM object_store_index;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TABLE object_store_index;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "ALTER TABLE object_store_index_upgrade " "RENAME TO object_store_index;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Update the |object_store| table to remove the UNIQUE constraint. rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "INSERT INTO object_store_upgrade " "SELECT * " "FROM object_store;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TABLE object_store;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "ALTER TABLE object_store_upgrade " "RENAME TO object_store;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Update the |database| table to include the origin, vacuum information, and // apply the WITHOUT ROWID optimization. nsCOMPtr<mozIStorageStatement> stmt; rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( "INSERT INTO database_upgrade " "SELECT name, :origin, version, 0, 0, 0 " "FROM database;" ), getter_AddRefs(stmt)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("origin"), aOrigin); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->Execute(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TABLE database;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "ALTER TABLE database_upgrade " "RENAME TO database;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } #ifdef DEBUG { // Make sure there's only one entry in the |database| table. nsCOMPtr<mozIStorageStatement> stmt; MOZ_ASSERT(NS_SUCCEEDED( aConnection->CreateStatement( NS_LITERAL_CSTRING("SELECT COUNT(*) " "FROM database;"), getter_AddRefs(stmt)))); bool hasResult; MOZ_ASSERT(NS_SUCCEEDED(stmt->ExecuteStep(&hasResult))); int64_t count; MOZ_ASSERT(NS_SUCCEEDED(stmt->GetInt64(0, &count))); MOZ_ASSERT(count == 1); } #endif // Recreate file table triggers. rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TRIGGER object_data_insert_trigger " "AFTER INSERT ON object_data " "WHEN NEW.file_ids IS NOT NULL " "BEGIN " "SELECT update_refcount(NULL, NEW.file_ids);" "END;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TRIGGER object_data_update_trigger " "AFTER UPDATE OF file_ids ON object_data " "WHEN OLD.file_ids IS NOT NULL OR NEW.file_ids IS NOT NULL " "BEGIN " "SELECT update_refcount(OLD.file_ids, NEW.file_ids);" "END;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TRIGGER object_data_delete_trigger " "AFTER DELETE ON object_data " "WHEN OLD.file_ids IS NOT NULL " "BEGIN " "SELECT update_refcount(OLD.file_ids, NULL);" "END;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Finally, turn on auto_vacuum mode. We use full auto_vacuum mode to reclaim // disk space on mobile devices (at the cost of some COMMIT speed), and // incremental auto_vacuum mode on desktop builds. rv = aConnection->ExecuteSimpleSQL( #ifdef IDB_MOBILE NS_LITERAL_CSTRING("PRAGMA auto_vacuum = FULL;") #else NS_LITERAL_CSTRING("PRAGMA auto_vacuum = INCREMENTAL;") #endif ); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->SetSchemaVersion(MakeSchemaVersion(18, 0)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } nsresult UpgradeSchemaFrom17_0To18_0(mozIStorageConnection* aConnection, const nsACString& aOrigin) { MOZ_ASSERT(aConnection); MOZ_ASSERT(!aOrigin.IsEmpty()); PROFILER_LABEL("IndexedDB", "UpgradeSchemaFrom17_0To18_0", js::ProfileEntry::Category::STORAGE); return UpgradeSchemaFrom17_0To18_0Helper::DoUpgrade(aConnection, aOrigin); } nsresult UpgradeSchemaFrom18_0To19_0(mozIStorageConnection* aConnection) { AssertIsOnIOThread(); MOZ_ASSERT(aConnection); nsresult rv; PROFILER_LABEL("IndexedDB", "UpgradeSchemaFrom18_0To19_0", js::ProfileEntry::Category::STORAGE); rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "ALTER TABLE object_store_index " "ADD COLUMN locale TEXT;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "ALTER TABLE object_store_index " "ADD COLUMN is_auto_locale BOOLEAN;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "ALTER TABLE index_data " "ADD COLUMN value_locale BLOB;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "ALTER TABLE unique_index_data " "ADD COLUMN value_locale BLOB;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE INDEX index_data_value_locale_index " "ON index_data (index_id, value_locale, object_data_key, value) " "WHERE value_locale IS NOT NULL;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE INDEX unique_index_data_value_locale_index " "ON unique_index_data (index_id, value_locale, object_data_key, value) " "WHERE value_locale IS NOT NULL;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->SetSchemaVersion(MakeSchemaVersion(19, 0)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } #if !defined(MOZ_B2G) class NormalJSContext; class UpgradeFileIdsFunction final : public mozIStorageFunction { RefPtr<FileManager> mFileManager; nsAutoPtr<NormalJSContext> mContext; public: UpgradeFileIdsFunction() { AssertIsOnIOThread(); } nsresult Init(nsIFile* aFMDirectory, mozIStorageConnection* aConnection); NS_DECL_ISUPPORTS private: ~UpgradeFileIdsFunction() { AssertIsOnIOThread(); if (mFileManager) { mFileManager->Invalidate(); } } NS_IMETHOD OnFunctionCall(mozIStorageValueArray* aArguments, nsIVariant** aResult) override; }; #endif // MOZ_B2G nsresult UpgradeSchemaFrom19_0To20_0(nsIFile* aFMDirectory, mozIStorageConnection* aConnection) { AssertIsOnIOThread(); MOZ_ASSERT(aConnection); PROFILER_LABEL("IndexedDB", "UpgradeSchemaFrom19_0To20_0", js::ProfileEntry::Category::STORAGE); #if defined(MOZ_B2G) // We don't have to do the upgrade of file ids on B2G. The old format was // only used by the previous single process implementation and B2G was // always multi process. This is a nice optimization since the upgrade needs // to deserialize all structured clones which reference a stored file or // a mutable file. nsresult rv = aConnection->SetSchemaVersion(MakeSchemaVersion(20, 0)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } #else // MOZ_B2G nsCOMPtr<mozIStorageStatement> stmt; nsresult rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( "SELECT count(*) " "FROM object_data " "WHERE file_ids IS NOT NULL" ), getter_AddRefs(stmt)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } int64_t count; { mozStorageStatementScoper scoper(stmt); bool hasResult; rv = stmt->ExecuteStep(&hasResult); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (NS_WARN_IF(!hasResult)) { MOZ_ASSERT(false, "This should never be possible!"); return NS_ERROR_FAILURE; } count = stmt->AsInt64(0); if (NS_WARN_IF(count < 0)) { MOZ_ASSERT(false, "This should never be possible!"); return NS_ERROR_FAILURE; } } if (count == 0) { // Nothing to upgrade. rv = aConnection->SetSchemaVersion(MakeSchemaVersion(20, 0)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } RefPtr<UpgradeFileIdsFunction> function = new UpgradeFileIdsFunction(); rv = function->Init(aFMDirectory, aConnection); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } NS_NAMED_LITERAL_CSTRING(functionName, "upgrade"); rv = aConnection->CreateFunction(functionName, 2, function); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Disable update trigger. rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TRIGGER object_data_update_trigger;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "UPDATE object_data " "SET file_ids = upgrade(file_ids, data) " "WHERE file_ids IS NOT NULL;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Enable update trigger. rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TRIGGER object_data_update_trigger " "AFTER UPDATE OF file_ids ON object_data " "FOR EACH ROW " "WHEN OLD.file_ids IS NOT NULL OR NEW.file_ids IS NOT NULL " "BEGIN " "SELECT update_refcount(OLD.file_ids, NEW.file_ids); " "END;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->RemoveFunction(functionName); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->SetSchemaVersion(MakeSchemaVersion(20, 0)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } #endif // MOZ_B2G return NS_OK; } class UpgradeIndexDataValuesFunction final : public mozIStorageFunction { public: UpgradeIndexDataValuesFunction() { AssertIsOnIOThread(); } NS_DECL_ISUPPORTS private: ~UpgradeIndexDataValuesFunction() { AssertIsOnIOThread(); } nsresult ReadOldCompressedIDVFromBlob(const uint8_t* aBlobData, uint32_t aBlobDataLength, nsTArray<IndexDataValue>& aIndexValues); NS_IMETHOD OnFunctionCall(mozIStorageValueArray* aArguments, nsIVariant** aResult) override; }; NS_IMPL_ISUPPORTS(UpgradeIndexDataValuesFunction, mozIStorageFunction) nsresult UpgradeIndexDataValuesFunction::ReadOldCompressedIDVFromBlob( const uint8_t* aBlobData, uint32_t aBlobDataLength, nsTArray<IndexDataValue>& aIndexValues) { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(aBlobData); MOZ_ASSERT(aBlobDataLength); MOZ_ASSERT(aIndexValues.IsEmpty()); const uint8_t* blobDataIter = aBlobData; const uint8_t* blobDataEnd = aBlobData + aBlobDataLength; int64_t indexId; bool unique; bool nextIndexIdAlreadyRead = false; while (blobDataIter < blobDataEnd) { if (!nextIndexIdAlreadyRead) { ReadCompressedIndexId(&blobDataIter, blobDataEnd, &indexId, &unique); } nextIndexIdAlreadyRead = false; if (NS_WARN_IF(blobDataIter == blobDataEnd)) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_FILE_CORRUPTED; } // Read key buffer length. const uint64_t keyBufferLength = ReadCompressedNumber(&blobDataIter, blobDataEnd); if (NS_WARN_IF(blobDataIter == blobDataEnd) || NS_WARN_IF(keyBufferLength > uint64_t(UINT32_MAX)) || NS_WARN_IF(blobDataIter + keyBufferLength > blobDataEnd)) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_FILE_CORRUPTED; } nsCString keyBuffer(reinterpret_cast<const char*>(blobDataIter), uint32_t(keyBufferLength)); blobDataIter += keyBufferLength; IndexDataValue idv(indexId, unique, Key(keyBuffer)); if (blobDataIter < blobDataEnd) { // Read either a sort key buffer length or an index id. uint64_t maybeIndexId = ReadCompressedNumber(&blobDataIter, blobDataEnd); // Locale-aware indexes haven't been around long enough to have any users, // we can safely assume all sort key buffer lengths will be zero. if (maybeIndexId != 0) { if (maybeIndexId % 2) { unique = true; maybeIndexId--; } else { unique = false; } indexId = maybeIndexId/2; nextIndexIdAlreadyRead = true; } } if (NS_WARN_IF(!aIndexValues.InsertElementSorted(idv, fallible))) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_OUT_OF_MEMORY; } } MOZ_ASSERT(blobDataIter == blobDataEnd); return NS_OK; } NS_IMETHODIMP UpgradeIndexDataValuesFunction::OnFunctionCall(mozIStorageValueArray* aArguments, nsIVariant** aResult) { MOZ_ASSERT(aArguments); MOZ_ASSERT(aResult); PROFILER_LABEL("IndexedDB", "UpgradeIndexDataValuesFunction::OnFunctionCall", js::ProfileEntry::Category::STORAGE); uint32_t argc; nsresult rv = aArguments->GetNumEntries(&argc); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (argc != 1) { NS_WARNING("Don't call me with the wrong number of arguments!"); return NS_ERROR_UNEXPECTED; } int32_t type; rv = aArguments->GetTypeOfIndex(0, &type); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (type != mozIStorageStatement::VALUE_TYPE_BLOB) { NS_WARNING("Don't call me with the wrong type of arguments!"); return NS_ERROR_UNEXPECTED; } const uint8_t* oldBlob; uint32_t oldBlobLength; rv = aArguments->GetSharedBlob(0, &oldBlobLength, &oldBlob); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } AutoTArray<IndexDataValue, 32> oldIdv; rv = ReadOldCompressedIDVFromBlob(oldBlob, oldBlobLength, oldIdv); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } UniqueFreePtr<uint8_t> newIdv; uint32_t newIdvLength; rv = MakeCompressedIndexDataValues(oldIdv, newIdv, &newIdvLength); std::pair<uint8_t*, int> data(newIdv.release(), newIdvLength); nsCOMPtr<nsIVariant> result = new storage::AdoptedBlobVariant(data); result.forget(aResult); return NS_OK; } nsresult UpgradeSchemaFrom20_0To21_0(mozIStorageConnection* aConnection) { // This should have been part of the 18 to 19 upgrade, where we changed the // layout of the index_data_values blobs but didn't upgrade the existing data. // See bug 1202788. AssertIsOnIOThread(); MOZ_ASSERT(aConnection); PROFILER_LABEL("IndexedDB", "UpgradeSchemaFrom20_0To21_0", js::ProfileEntry::Category::STORAGE); RefPtr<UpgradeIndexDataValuesFunction> function = new UpgradeIndexDataValuesFunction(); NS_NAMED_LITERAL_CSTRING(functionName, "upgrade_idv"); nsresult rv = aConnection->CreateFunction(functionName, 1, function); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "UPDATE object_data " "SET index_data_values = upgrade_idv(index_data_values) " "WHERE index_data_values IS NOT NULL;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->RemoveFunction(functionName); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->SetSchemaVersion(MakeSchemaVersion(21, 0)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } nsresult UpgradeSchemaFrom21_0To22_0(mozIStorageConnection* aConnection) { // The only change between 21 and 22 was a different structured clone format, // but it's backwards-compatible. nsresult rv = aConnection->SetSchemaVersion(MakeSchemaVersion(22, 0)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } nsresult UpgradeSchemaFrom22_0To23_0(mozIStorageConnection* aConnection, const nsACString& aOrigin) { AssertIsOnIOThread(); MOZ_ASSERT(aConnection); MOZ_ASSERT(!aOrigin.IsEmpty()); PROFILER_LABEL("IndexedDB", "UpgradeSchemaFrom22_0To23_0", js::ProfileEntry::Category::STORAGE); nsCOMPtr<mozIStorageStatement> stmt; nsresult rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( "UPDATE database " "SET origin = :origin;" ), getter_AddRefs(stmt)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("origin"), aOrigin); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->Execute(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->SetSchemaVersion(MakeSchemaVersion(23, 0)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } nsresult UpgradeSchemaFrom23_0To24_0(mozIStorageConnection* aConnection) { // The only change between 23 and 24 was a different structured clone format, // but it's backwards-compatible. nsresult rv = aConnection->SetSchemaVersion(MakeSchemaVersion(24, 0)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } nsresult UpgradeSchemaFrom24_0To25_0(mozIStorageConnection* aConnection) { // The changes between 24 and 25 were an upgraded snappy library, a different // structured clone format and a different file_ds format. But everything is // backwards-compatible. nsresult rv = aConnection->SetSchemaVersion(MakeSchemaVersion(25, 0)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } nsresult GetDatabaseFileURL(nsIFile* aDatabaseFile, PersistenceType aPersistenceType, const nsACString& aGroup, const nsACString& aOrigin, uint32_t aTelemetryId, nsIFileURL** aResult) { MOZ_ASSERT(aDatabaseFile); MOZ_ASSERT(aResult); nsresult rv; nsCOMPtr<nsIProtocolHandler> protocolHandler( do_GetService(NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX "file", &rv)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsCOMPtr<nsIFileProtocolHandler> fileHandler( do_QueryInterface(protocolHandler, &rv)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsCOMPtr<nsIURI> uri; rv = fileHandler->NewFileURI(aDatabaseFile, getter_AddRefs(uri)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsCOMPtr<nsIFileURL> fileUrl = do_QueryInterface(uri); MOZ_ASSERT(fileUrl); nsAutoCString type; PersistenceTypeToText(aPersistenceType, type); nsAutoCString telemetryFilenameClause; if (aTelemetryId) { telemetryFilenameClause.AssignLiteral("&telemetryFilename=indexedDB-"); telemetryFilenameClause.AppendInt(aTelemetryId); telemetryFilenameClause.AppendLiteral(".sqlite"); } rv = fileUrl->SetQuery(NS_LITERAL_CSTRING("persistenceType=") + type + NS_LITERAL_CSTRING("&group=") + aGroup + NS_LITERAL_CSTRING("&origin=") + aOrigin + NS_LITERAL_CSTRING("&cache=private") + telemetryFilenameClause); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } fileUrl.forget(aResult); return NS_OK; } nsresult SetDefaultPragmas(mozIStorageConnection* aConnection) { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(aConnection); static const char 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;" ; nsresult rv = aConnection->ExecuteSimpleSQL( nsDependentCString(kBuiltInPragmas, LiteralStringLength(kBuiltInPragmas))); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsAutoCString pragmaStmt; pragmaStmt.AssignLiteral("PRAGMA synchronous = "); if (IndexedDatabaseManager::FullSynchronous()) { pragmaStmt.AppendLiteral("FULL"); } else { pragmaStmt.AppendLiteral("NORMAL"); } pragmaStmt.Append(';'); rv = aConnection->ExecuteSimpleSQL(pragmaStmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } #ifndef IDB_MOBILE if (kSQLiteGrowthIncrement) { // This is just an optimization so ignore the failure if the disk is // currently too full. rv = aConnection->SetGrowthIncrement(kSQLiteGrowthIncrement, EmptyCString()); if (rv != NS_ERROR_FILE_TOO_BIG && NS_WARN_IF(NS_FAILED(rv))) { return rv; } } #endif // IDB_MOBILE return NS_OK; } nsresult SetJournalMode(mozIStorageConnection* aConnection) { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(aConnection); // Try enabling WAL mode. This can fail in various circumstances so we have to // check the results here. NS_NAMED_LITERAL_CSTRING(journalModeQueryStart, "PRAGMA journal_mode = "); NS_NAMED_LITERAL_CSTRING(journalModeWAL, "wal"); nsCOMPtr<mozIStorageStatement> stmt; nsresult rv = aConnection->CreateStatement(journalModeQueryStart + journalModeWAL, getter_AddRefs(stmt)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } bool hasResult; rv = stmt->ExecuteStep(&hasResult); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(hasResult); nsCString journalMode; rv = stmt->GetUTF8String(0, journalMode); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (journalMode.Equals(journalModeWAL)) { // WAL mode successfully enabled. Maybe set limits on its size here. if (kMaxWALPages >= 0) { nsAutoCString pageCount; pageCount.AppendInt(kMaxWALPages); rv = aConnection->ExecuteSimpleSQL( NS_LITERAL_CSTRING("PRAGMA wal_autocheckpoint = ") + pageCount); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } } else { NS_WARNING("Failed to set WAL mode, falling back to normal journal mode."); #ifdef IDB_MOBILE rv = aConnection->ExecuteSimpleSQL(journalModeQueryStart + NS_LITERAL_CSTRING("truncate")); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } #endif } return NS_OK; } template <class FileOrURLType> struct StorageOpenTraits; template <> struct StorageOpenTraits<nsIFileURL*> { static nsresult Open(mozIStorageService* aStorageService, nsIFileURL* aFileURL, mozIStorageConnection** aConnection) { return aStorageService->OpenDatabaseWithFileURL(aFileURL, aConnection); } #ifdef DEBUG static void GetPath(nsIFileURL* aFileURL, nsCString& aPath) { MOZ_ALWAYS_SUCCEEDS(aFileURL->GetFileName(aPath)); } #endif }; template <> struct StorageOpenTraits<nsIFile*> { static nsresult Open(mozIStorageService* aStorageService, nsIFile* aFile, mozIStorageConnection** aConnection) { return aStorageService->OpenUnsharedDatabase(aFile, aConnection); } #ifdef DEBUG static void GetPath(nsIFile* aFile, nsCString& aPath) { nsString path; MOZ_ALWAYS_SUCCEEDS(aFile->GetPath(path)); aPath.AssignWithConversion(path); } #endif }; template <template <class> class SmartPtr, class FileOrURLType> struct StorageOpenTraits<SmartPtr<FileOrURLType>> : public StorageOpenTraits<FileOrURLType*> { }; template <class FileOrURLType> nsresult OpenDatabaseAndHandleBusy(mozIStorageService* aStorageService, FileOrURLType aFileOrURL, mozIStorageConnection** aConnection) { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(aStorageService); MOZ_ASSERT(aFileOrURL); MOZ_ASSERT(aConnection); nsCOMPtr<mozIStorageConnection> connection; nsresult rv = StorageOpenTraits<FileOrURLType>::Open(aStorageService, aFileOrURL, getter_AddRefs(connection)); if (rv == NS_ERROR_STORAGE_BUSY) { #ifdef DEBUG { nsCString path; StorageOpenTraits<FileOrURLType>::GetPath(aFileOrURL, 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. TimeStamp start = TimeStamp::NowLoRes(); while (true) { PR_Sleep(PR_MillisecondsToInterval(100)); rv = StorageOpenTraits<FileOrURLType>::Open(aStorageService, aFileOrURL, getter_AddRefs(connection)); if (rv != NS_ERROR_STORAGE_BUSY || TimeStamp::NowLoRes() - start > TimeDuration::FromSeconds(10)) { break; } } } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } connection.forget(aConnection); return NS_OK; } nsresult CreateStorageConnection(nsIFile* aDBFile, nsIFile* aFMDirectory, const nsAString& aName, PersistenceType aPersistenceType, const nsACString& aGroup, const nsACString& aOrigin, uint32_t aTelemetryId, mozIStorageConnection** aConnection) { AssertIsOnIOThread(); MOZ_ASSERT(aDBFile); MOZ_ASSERT(aFMDirectory); MOZ_ASSERT(aConnection); PROFILER_LABEL("IndexedDB", "CreateStorageConnection", js::ProfileEntry::Category::STORAGE); nsresult rv; bool exists; if (IndexedDatabaseManager::InLowDiskSpaceMode()) { rv = aDBFile->Exists(&exists); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (!exists) { NS_WARNING("Refusing to create database because disk space is low!"); return NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR; } } nsCOMPtr<nsIFileURL> dbFileUrl; rv = GetDatabaseFileURL(aDBFile, aPersistenceType, aGroup, aOrigin, aTelemetryId, getter_AddRefs(dbFileUrl)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsCOMPtr<mozIStorageService> ss = do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID, &rv); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsCOMPtr<mozIStorageConnection> connection; rv = OpenDatabaseAndHandleBusy(ss, dbFileUrl, getter_AddRefs(connection)); if (rv == NS_ERROR_FILE_CORRUPTED) { // 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. if (aName.IsVoid()) { return rv; } // Nuke the database file. rv = aDBFile->Remove(false); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aFMDirectory->Exists(&exists); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (exists) { bool isDirectory; rv = aFMDirectory->IsDirectory(&isDirectory); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (NS_WARN_IF(!isDirectory)) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } rv = aFMDirectory->Remove(true); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } rv = OpenDatabaseAndHandleBusy(ss, dbFileUrl, getter_AddRefs(connection)); } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = SetDefaultPragmas(connection); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = connection->EnableModule(NS_LITERAL_CSTRING("filesystem")); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Check to make sure that the database schema is correct. int32_t schemaVersion; rv = connection->GetSchemaVersion(&schemaVersion); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Unknown schema will fail origin initialization too. if (!schemaVersion && aName.IsVoid()) { IDB_WARNING("Unable to open IndexedDB database, schema is not set!"); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } if (schemaVersion > kSQLiteSchemaVersion) { IDB_WARNING("Unable to open IndexedDB database, schema is too high!"); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } bool journalModeSet = false; if (schemaVersion != kSQLiteSchemaVersion) { const bool newDatabase = !schemaVersion; if (newDatabase) { // Set the page size first. if (kSQLitePageSizeOverride) { rv = connection->ExecuteSimpleSQL( nsPrintfCString("PRAGMA page_size = %lu;", kSQLitePageSizeOverride) ); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } // We have to set the auto_vacuum mode before opening a transaction. rv = 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). NS_LITERAL_CSTRING("PRAGMA auto_vacuum = FULL;") #else // Turn on incremental auto_vacuum mode on desktop builds. NS_LITERAL_CSTRING("PRAGMA auto_vacuum = INCREMENTAL;") #endif ); if (rv == 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. rv = NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR; } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = SetJournalMode(connection); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } 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( NS_LITERAL_CSTRING("PRAGMA foreign_keys = OFF;"))); #endif } bool vacuumNeeded = false; mozStorageTransaction transaction(connection, false, mozIStorageConnection::TRANSACTION_IMMEDIATE); if (newDatabase) { rv = CreateTables(connection); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(NS_SUCCEEDED(connection->GetSchemaVersion(&schemaVersion))); MOZ_ASSERT(schemaVersion == kSQLiteSchemaVersion); nsCOMPtr<mozIStorageStatement> stmt; nsresult rv = connection->CreateStatement(NS_LITERAL_CSTRING( "INSERT INTO database (name, origin) " "VALUES (:name, :origin)" ), getter_AddRefs(stmt)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->BindStringByName(NS_LITERAL_CSTRING("name"), aName); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("origin"), aOrigin); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->Execute(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } else { // This logic needs to change next time we change the schema! static_assert(kSQLiteSchemaVersion == int32_t((25 << 4) + 0), "Upgrade function needed due to schema version increase."); while (schemaVersion != kSQLiteSchemaVersion) { if (schemaVersion == 4) { rv = UpgradeSchemaFrom4To5(connection); } else if (schemaVersion == 5) { rv = UpgradeSchemaFrom5To6(connection); } else if (schemaVersion == 6) { rv = UpgradeSchemaFrom6To7(connection); } else if (schemaVersion == 7) { rv = UpgradeSchemaFrom7To8(connection); } else if (schemaVersion == 8) { rv = UpgradeSchemaFrom8To9_0(connection); vacuumNeeded = true; } else if (schemaVersion == MakeSchemaVersion(9, 0)) { rv = UpgradeSchemaFrom9_0To10_0(connection); } else if (schemaVersion == MakeSchemaVersion(10, 0)) { rv = UpgradeSchemaFrom10_0To11_0(connection); } else if (schemaVersion == MakeSchemaVersion(11, 0)) { rv = UpgradeSchemaFrom11_0To12_0(connection); } else if (schemaVersion == MakeSchemaVersion(12, 0)) { rv = UpgradeSchemaFrom12_0To13_0(connection, &vacuumNeeded); } else if (schemaVersion == MakeSchemaVersion(13, 0)) { rv = UpgradeSchemaFrom13_0To14_0(connection); } else if (schemaVersion == MakeSchemaVersion(14, 0)) { rv = UpgradeSchemaFrom14_0To15_0(connection); } else if (schemaVersion == MakeSchemaVersion(15, 0)) { rv = UpgradeSchemaFrom15_0To16_0(connection); } else if (schemaVersion == MakeSchemaVersion(16, 0)) { rv = UpgradeSchemaFrom16_0To17_0(connection); } else if (schemaVersion == MakeSchemaVersion(17, 0)) { rv = UpgradeSchemaFrom17_0To18_0(connection, aOrigin); vacuumNeeded = true; } else if (schemaVersion == MakeSchemaVersion(18, 0)) { rv = UpgradeSchemaFrom18_0To19_0(connection); } else if (schemaVersion == MakeSchemaVersion(19, 0)) { rv = UpgradeSchemaFrom19_0To20_0(aFMDirectory, connection); } else if (schemaVersion == MakeSchemaVersion(20, 0)) { rv = UpgradeSchemaFrom20_0To21_0(connection); } else if (schemaVersion == MakeSchemaVersion(21, 0)) { rv = UpgradeSchemaFrom21_0To22_0(connection); } else if (schemaVersion == MakeSchemaVersion(22, 0)) { rv = UpgradeSchemaFrom22_0To23_0(connection, aOrigin); } else if (schemaVersion == MakeSchemaVersion(23, 0)) { rv = UpgradeSchemaFrom23_0To24_0(connection); } else if (schemaVersion == MakeSchemaVersion(24, 0)) { rv = UpgradeSchemaFrom24_0To25_0(connection); } else { IDB_WARNING("Unable to open IndexedDB database, no upgrade path is " "available!"); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = connection->GetSchemaVersion(&schemaVersion); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } MOZ_ASSERT(schemaVersion == kSQLiteSchemaVersion); } rv = transaction.Commit(); if (rv == 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. rv = NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR; } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } #ifdef DEBUG if (!newDatabase) { // Re-enable foreign key support after doing a foreign key check. nsCOMPtr<mozIStorageStatement> checkStmt; MOZ_ALWAYS_SUCCEEDS( connection->CreateStatement( NS_LITERAL_CSTRING("PRAGMA foreign_key_check;"), getter_AddRefs(checkStmt))); bool hasResult; MOZ_ALWAYS_SUCCEEDS(checkStmt->ExecuteStep(&hasResult)); MOZ_ASSERT(!hasResult, "Database has inconsisistent foreign keys!"); MOZ_ALWAYS_SUCCEEDS( connection->ExecuteSimpleSQL( NS_LITERAL_CSTRING("PRAGMA foreign_keys = OFF;"))); } #endif if (kSQLitePageSizeOverride && !newDatabase) { nsCOMPtr<mozIStorageStatement> stmt; rv = connection->CreateStatement(NS_LITERAL_CSTRING( "PRAGMA page_size;" ), getter_AddRefs(stmt)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } bool hasResult; rv = stmt->ExecuteStep(&hasResult); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(hasResult); int32_t pageSize; rv = stmt->GetInt32(0, &pageSize); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(pageSize >= 512 && pageSize <= 65536); if (kSQLitePageSizeOverride != uint32_t(pageSize)) { // We must not be in WAL journal mode to change the page size. rv = connection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "PRAGMA journal_mode = DELETE;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = connection->CreateStatement(NS_LITERAL_CSTRING( "PRAGMA journal_mode;" ), getter_AddRefs(stmt)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->ExecuteStep(&hasResult); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(hasResult); nsCString journalMode; rv = stmt->GetUTF8String(0, journalMode); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (journalMode.EqualsLiteral("delete")) { // Successfully set to rollback journal mode so changing the page size // is possible with a VACUUM. rv = connection->ExecuteSimpleSQL( nsPrintfCString("PRAGMA page_size = %lu;", kSQLitePageSizeOverride) ); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // 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) { rv = connection->ExecuteSimpleSQL(NS_LITERAL_CSTRING("VACUUM;")); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } if (newDatabase || vacuumNeeded) { if (journalModeSet) { // Make sure we checkpoint to get an accurate file size. rv = connection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "PRAGMA wal_checkpoint(FULL);" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } int64_t fileSize; rv = aDBFile->GetFileSize(&fileSize); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(fileSize > 0); PRTime vacuumTime = PR_Now(); MOZ_ASSERT(vacuumTime); nsCOMPtr<mozIStorageStatement> vacuumTimeStmt; rv = connection->CreateStatement(NS_LITERAL_CSTRING( "UPDATE database " "SET last_vacuum_time = :time" ", last_vacuum_size = :size;" ), getter_AddRefs(vacuumTimeStmt)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = vacuumTimeStmt->BindInt64ByName(NS_LITERAL_CSTRING("time"), vacuumTime); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = vacuumTimeStmt->BindInt64ByName(NS_LITERAL_CSTRING("size"), fileSize); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = vacuumTimeStmt->Execute(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } } if (!journalModeSet) { rv = SetJournalMode(connection); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } connection.forget(aConnection); return NS_OK; } already_AddRefed<nsIFile> GetFileForPath(const nsAString& aPath) { MOZ_ASSERT(!aPath.IsEmpty()); nsCOMPtr<nsIFile> file = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID); if (NS_WARN_IF(!file)) { return nullptr; } if (NS_WARN_IF(NS_FAILED(file->InitWithPath(aPath)))) { return nullptr; } return file.forget(); } nsresult GetStorageConnection(nsIFile* aDatabaseFile, PersistenceType aPersistenceType, const nsACString& aGroup, const nsACString& aOrigin, uint32_t aTelemetryId, mozIStorageConnection** aConnection) { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(aDatabaseFile); MOZ_ASSERT(aConnection); PROFILER_LABEL("IndexedDB", "GetStorageConnection", js::ProfileEntry::Category::STORAGE); bool exists; nsresult rv = aDatabaseFile->Exists(&exists); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (NS_WARN_IF(!exists)) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } nsCOMPtr<nsIFileURL> dbFileUrl; rv = GetDatabaseFileURL(aDatabaseFile, aPersistenceType, aGroup, aOrigin, aTelemetryId, getter_AddRefs(dbFileUrl)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsCOMPtr<mozIStorageService> ss = do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID, &rv); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsCOMPtr<mozIStorageConnection> connection; rv = OpenDatabaseAndHandleBusy(ss, dbFileUrl, getter_AddRefs(connection)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = SetDefaultPragmas(connection); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = SetJournalMode(connection); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } connection.forget(aConnection); return NS_OK; } nsresult GetStorageConnection(const nsAString& aDatabaseFilePath, PersistenceType aPersistenceType, const nsACString& aGroup, const nsACString& aOrigin, uint32_t aTelemetryId, mozIStorageConnection** aConnection) { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(!aDatabaseFilePath.IsEmpty()); MOZ_ASSERT(StringEndsWith(aDatabaseFilePath, NS_LITERAL_STRING(".sqlite"))); MOZ_ASSERT(aConnection); nsCOMPtr<nsIFile> dbFile = GetFileForPath(aDatabaseFilePath); if (NS_WARN_IF(!dbFile)) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } return GetStorageConnection(dbFile, aPersistenceType, aGroup, aOrigin, aTelemetryId, aConnection); } /******************************************************************************* * ConnectionPool declarations ******************************************************************************/ class DatabaseConnection final { friend class ConnectionPool; enum class CheckpointMode { Full, Restart, Truncate }; public: class AutoSavepoint; class CachedStatement; class UpdateRefcountFunction; private: nsCOMPtr<mozIStorageConnection> mStorageConnection; RefPtr<FileManager> mFileManager; nsInterfaceHashtable<nsCStringHashKey, mozIStorageStatement> mCachedStatements; RefPtr<UpdateRefcountFunction> mUpdateRefcountFunction; RefPtr<QuotaObject> mQuotaObject; RefPtr<QuotaObject> mJournalQuotaObject; bool mInReadTransaction; bool mInWriteTransaction; #ifdef DEBUG uint32_t mDEBUGSavepointCount; PRThread* mDEBUGThread; #endif public: void AssertIsOnConnectionThread() const { MOZ_ASSERT(mDEBUGThread); MOZ_ASSERT(PR_GetCurrentThread() == mDEBUGThread); } NS_INLINE_DECL_THREADSAFE_REFCOUNTING(DatabaseConnection) mozIStorageConnection* GetStorageConnection() const { if (mStorageConnection) { AssertIsOnConnectionThread(); return mStorageConnection; } return nullptr; } UpdateRefcountFunction* GetUpdateRefcountFunction() const { AssertIsOnConnectionThread(); return mUpdateRefcountFunction; } nsresult GetCachedStatement(const nsACString& aQuery, CachedStatement* aCachedStatement); 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(mozIStorageConnection* aStorageConnection, FileManager* aFileManager); ~DatabaseConnection(); nsresult Init(); nsresult CheckpointInternal(CheckpointMode aMode); nsresult GetFreelistCount(CachedStatement& aCachedStatement, uint32_t* aFreelistCount); nsresult ReclaimFreePagesWhileIdle(CachedStatement& aFreelistStatement, CachedStatement& aRollbackStatement, uint32_t aFreelistCount, bool aNeedsCheckpoint, bool* aFreedSomePages); nsresult GetFileSize(const nsAString& aPath, int64_t* aResult); }; class MOZ_STACK_CLASS DatabaseConnection::AutoSavepoint final { DatabaseConnection* mConnection; #ifdef DEBUG const TransactionBase* mDEBUGTransaction; #endif public: AutoSavepoint(); ~AutoSavepoint(); nsresult Start(const TransactionBase* aConnection); nsresult Commit(); }; class DatabaseConnection::CachedStatement final { friend class DatabaseConnection; nsCOMPtr<mozIStorageStatement> mStatement; Maybe<mozStorageStatementScoper> mScoper; #ifdef DEBUG DatabaseConnection* mDEBUGConnection; #endif public: CachedStatement(); ~CachedStatement(); void AssertIsOnConnectionThread() const { #ifdef DEBUG if (mDEBUGConnection) { mDEBUGConnection->AssertIsOnConnectionThread(); } MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!IsOnBackgroundThread()); #endif } operator mozIStorageStatement*() const; mozIStorageStatement* operator->() const MOZ_NO_ADDREF_RELEASE_ON_RETURN; void Reset(); private: // Only called by DatabaseConnection. void Assign(DatabaseConnection* aConnection, already_AddRefed<mozIStorageStatement> aStatement); // No funny business allowed. CachedStatement(const CachedStatement&) = delete; CachedStatement& operator=(const CachedStatement&) = delete; }; class DatabaseConnection::UpdateRefcountFunction final : public mozIStorageFunction { class DatabaseUpdateFunction; class FileInfoEntry; enum class UpdateType { Increment, Decrement }; DatabaseConnection* mConnection; FileManager* mFileManager; nsClassHashtable<nsUint64HashKey, FileInfoEntry> mFileInfoEntries; nsDataHashtable<nsUint64HashKey, FileInfoEntry*> mSavepointEntriesIndex; nsTArray<int64_t> mJournalsToCreateBeforeCommit; nsTArray<int64_t> mJournalsToRemoveAfterCommit; nsTArray<int64_t> mJournalsToRemoveAfterAbort; bool mInSavepoint; public: NS_DECL_ISUPPORTS NS_DECL_MOZISTORAGEFUNCTION UpdateRefcountFunction(DatabaseConnection* aConnection, FileManager* aFileManager); nsresult WillCommit(); void DidCommit(); void DidAbort(); void StartSavepoint(); void ReleaseSavepoint(); void RollbackSavepoint(); void Reset(); private: ~UpdateRefcountFunction() { } nsresult ProcessValue(mozIStorageValueArray* aValues, int32_t aIndex, UpdateType aUpdateType); nsresult CreateJournals(); nsresult RemoveJournals(const nsTArray<int64_t>& aJournals); }; class DatabaseConnection::UpdateRefcountFunction::DatabaseUpdateFunction final { CachedStatement mUpdateStatement; CachedStatement mSelectStatement; CachedStatement mInsertStatement; UpdateRefcountFunction* mFunction; nsresult mErrorCode; public: explicit DatabaseUpdateFunction(UpdateRefcountFunction* aFunction) : mFunction(aFunction) , mErrorCode(NS_OK) { MOZ_COUNT_CTOR( DatabaseConnection::UpdateRefcountFunction::DatabaseUpdateFunction); } ~DatabaseUpdateFunction() { MOZ_COUNT_DTOR( DatabaseConnection::UpdateRefcountFunction::DatabaseUpdateFunction); } bool Update(int64_t aId, int32_t aDelta); nsresult ErrorCode() const { return mErrorCode; } private: nsresult UpdateInternal(int64_t aId, int32_t aDelta); }; class DatabaseConnection::UpdateRefcountFunction::FileInfoEntry final { friend class UpdateRefcountFunction; RefPtr<FileInfo> mFileInfo; int32_t mDelta; int32_t mSavepointDelta; public: explicit FileInfoEntry(FileInfo* aFileInfo) : mFileInfo(aFileInfo) , mDelta(0) , mSavepointDelta(0) { MOZ_COUNT_CTOR(DatabaseConnection::UpdateRefcountFunction::FileInfoEntry); } ~FileInfoEntry() { MOZ_COUNT_DTOR(DatabaseConnection::UpdateRefcountFunction::FileInfoEntry); } }; class ConnectionPool final { public: class FinishCallback; private: class ConnectionRunnable; class CloseConnectionRunnable; struct DatabaseInfo; struct DatabasesCompleteCallback; class FinishCallbackWrapper; class IdleConnectionRunnable; struct IdleDatabaseInfo; struct IdleResource; struct IdleThreadInfo; struct ThreadInfo; class ThreadRunnable; class TransactionInfo; struct TransactionInfoPair; // This mutex guards mDatabases, see below. Mutex mDatabasesMutex; nsTArray<IdleThreadInfo> mIdleThreads; nsTArray<IdleDatabaseInfo> mIdleDatabases; nsTArray<DatabaseInfo*> mDatabasesPerformingIdleMaintenance; nsCOMPtr<nsITimer> 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<nsCStringHashKey, DatabaseInfo> mDatabases; nsClassHashtable<nsUint64HashKey, TransactionInfo> mTransactions; nsTArray<TransactionInfo*> mQueuedTransactions; nsTArray<nsAutoPtr<DatabasesCompleteCallback>> mCompleteCallbacks; uint64_t mNextTransactionId; uint32_t mTotalThreadCount; bool mShutdownRequested; bool mShutdownComplete; #ifdef DEBUG PRThread* mDEBUGOwningThread; #endif public: ConnectionPool(); void AssertIsOnOwningThread() const #ifdef DEBUG ; #else { } #endif nsresult GetOrCreateConnection(const Database* aDatabase, DatabaseConnection** aConnection); uint64_t Start(const nsID& aBackgroundChildLoggingId, const nsACString& aDatabaseId, int64_t aLoggingSerialNumber, const nsTArray<nsString>& 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 WaitForDatabasesToComplete(nsTArray<nsCString>&& aDatabaseIds, nsIRunnable* aCallback); void Shutdown(); NS_INLINE_DECL_REFCOUNTING(ConnectionPool) private: ~ConnectionPool(); static void IdleTimerCallback(nsITimer* aTimer, void* aClosure); 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(DatabasesCompleteCallback* aCallback); void PerformIdleDatabaseMaintenance(DatabaseInfo* aDatabaseInfo); void CloseDatabase(DatabaseInfo* aDatabaseInfo); bool CloseDatabaseWhenIdleInternal(const nsACString& aDatabaseId); }; class ConnectionPool::ConnectionRunnable : public Runnable { protected: DatabaseInfo* mDatabaseInfo; nsCOMPtr<nsIEventTarget> mOwningThread; explicit ConnectionRunnable(DatabaseInfo* aDatabaseInfo); virtual ~ConnectionRunnable() { } }; class ConnectionPool::IdleConnectionRunnable final : public ConnectionRunnable { bool mNeedsCheckpoint; public: IdleConnectionRunnable(DatabaseInfo* aDatabaseInfo, bool aNeedsCheckpoint) : ConnectionRunnable(aDatabaseInfo) , mNeedsCheckpoint(aNeedsCheckpoint) { } NS_DECL_ISUPPORTS_INHERITED private: ~IdleConnectionRunnable() { } NS_DECL_NSIRUNNABLE }; class ConnectionPool::CloseConnectionRunnable final : public ConnectionRunnable { public: explicit CloseConnectionRunnable(DatabaseInfo* aDatabaseInfo) : ConnectionRunnable(aDatabaseInfo) { } NS_DECL_ISUPPORTS_INHERITED private: ~CloseConnectionRunnable() { } NS_DECL_NSIRUNNABLE }; struct ConnectionPool::ThreadInfo { nsCOMPtr<nsIThread> mThread; RefPtr<ThreadRunnable> mRunnable; ThreadInfo(); explicit ThreadInfo(const ThreadInfo& aOther); ~ThreadInfo(); }; struct ConnectionPool::DatabaseInfo final { friend class nsAutoPtr<DatabaseInfo>; RefPtr<ConnectionPool> mConnectionPool; const nsCString mDatabaseId; RefPtr<DatabaseConnection> mConnection; nsClassHashtable<nsStringHashKey, TransactionInfoPair> mBlockingTransactions; nsTArray<TransactionInfo*> mTransactionsScheduledDuringClose; nsTArray<TransactionInfo*> mScheduledWriteTransactions; TransactionInfo* mRunningWriteTransaction; ThreadInfo mThreadInfo; uint32_t mReadTransactionCount; uint32_t mWriteTransactionCount; bool mNeedsCheckpoint; bool mIdle; bool mCloseOnIdle; bool mClosing; #ifdef DEBUG PRThread* mDEBUGConnectionThread; #endif DatabaseInfo(ConnectionPool* aConnectionPool, const nsACString& aDatabaseId); void AssertIsOnConnectionThread() const { MOZ_ASSERT(mDEBUGConnectionThread); MOZ_ASSERT(PR_GetCurrentThread() == mDEBUGConnectionThread); } uint64_t TotalTransactionCount() const { return mReadTransactionCount + mWriteTransactionCount; } private: ~DatabaseInfo(); DatabaseInfo(const DatabaseInfo&) = delete; DatabaseInfo& operator=(const DatabaseInfo&) = delete; }; struct ConnectionPool::DatabasesCompleteCallback final { friend class nsAutoPtr<DatabasesCompleteCallback>; nsTArray<nsCString> mDatabaseIds; nsCOMPtr<nsIRunnable> mCallback; DatabasesCompleteCallback(nsTArray<nsCString>&& aDatabaseIds, nsIRunnable* aCallback); private: ~DatabasesCompleteCallback(); }; 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() { } virtual ~FinishCallback() { } }; class ConnectionPool::FinishCallbackWrapper final : public Runnable { RefPtr<ConnectionPool> mConnectionPool; RefPtr<FinishCallback> mCallback; nsCOMPtr<nsIEventTarget> mOwningThread; uint64_t mTransactionId; bool mHasRunOnce; public: FinishCallbackWrapper(ConnectionPool* aConnectionPool, uint64_t aTransactionId, FinishCallback* aCallback); NS_DECL_ISUPPORTS_INHERITED private: ~FinishCallbackWrapper(); NS_DECL_NSIRUNNABLE }; struct ConnectionPool::IdleResource { TimeStamp mIdleTime; protected: explicit IdleResource(const TimeStamp& aIdleTime); explicit IdleResource(const IdleResource& aOther) = delete; ~IdleResource(); }; struct ConnectionPool::IdleDatabaseInfo final : public IdleResource { DatabaseInfo* mDatabaseInfo; public: MOZ_IMPLICIT IdleDatabaseInfo(DatabaseInfo* aDatabaseInfo); explicit IdleDatabaseInfo(const IdleDatabaseInfo& aOther) = delete; ~IdleDatabaseInfo(); bool operator==(const IdleDatabaseInfo& aOther) const { return mDatabaseInfo == aOther.mDatabaseInfo; } bool operator<(const IdleDatabaseInfo& aOther) const { return mIdleTime < aOther.mIdleTime; } }; struct ConnectionPool::IdleThreadInfo final : public IdleResource { ThreadInfo mThreadInfo; public: // Boo, this is needed because nsTArray::InsertElementSorted() doesn't yet // work with rvalue references. MOZ_IMPLICIT IdleThreadInfo(const ThreadInfo& aThreadInfo); explicit IdleThreadInfo(const IdleThreadInfo& aOther) = delete; ~IdleThreadInfo(); bool operator==(const IdleThreadInfo& aOther) const { return mThreadInfo.mRunnable == aOther.mThreadInfo.mRunnable && mThreadInfo.mThread == aOther.mThreadInfo.mThread; } bool operator<(const IdleThreadInfo& aOther) const { return mIdleTime < aOther.mIdleTime; } }; class ConnectionPool::ThreadRunnable final : public Runnable { // Only touched on the background thread. static uint32_t sNextSerialNumber; // Set at construction for logging. const uint32_t mSerialNumber; // These two values are only modified on the connection thread. bool mFirstRun; bool mContinueRunning; public: ThreadRunnable(); NS_DECL_ISUPPORTS_INHERITED uint32_t SerialNumber() const { return mSerialNumber; } private: ~ThreadRunnable(); NS_DECL_NSIRUNNABLE }; class ConnectionPool::TransactionInfo final { friend class nsAutoPtr<TransactionInfo>; nsTHashtable<nsPtrHashKey<TransactionInfo>> mBlocking; nsTArray<TransactionInfo*> mBlockingOrdered; public: DatabaseInfo* mDatabaseInfo; const nsID mBackgroundChildLoggingId; const nsCString mDatabaseId; const uint64_t mTransactionId; const int64_t mLoggingSerialNumber; const nsTArray<nsString> mObjectStoreNames; nsTHashtable<nsPtrHashKey<TransactionInfo>> mBlockedOn; nsTArray<nsCOMPtr<nsIRunnable>> mQueuedRunnables; const bool mIsWriteTransaction; bool mRunning; #ifdef DEBUG bool mFinished; #endif TransactionInfo(DatabaseInfo* aDatabaseInfo, const nsID& aBackgroundChildLoggingId, const nsACString& aDatabaseId, uint64_t aTransactionId, int64_t aLoggingSerialNumber, const nsTArray<nsString>& aObjectStoreNames, bool aIsWriteTransaction, TransactionDatabaseOperationBase* aTransactionOp); void AddBlockingTransaction(TransactionInfo* aTransactionInfo); void RemoveBlockingTransactions(); private: ~TransactionInfo(); void MaybeUnblock(TransactionInfo* aTransactionInfo); }; struct ConnectionPool::TransactionInfoPair final { friend class nsAutoPtr<TransactionInfoPair>; // Multiple reading transactions can block future writes. nsTArray<TransactionInfo*> mLastBlockingWrites; // But only a single writing transaction can block future reads. TransactionInfo* mLastBlockingReads; TransactionInfoPair(); private: ~TransactionInfoPair(); }; /******************************************************************************* * Actor class declarations ******************************************************************************/ class DatabaseOperationBase : public Runnable , public mozIStorageProgressHandler { friend class UpgradeFileIdsFunction; protected: class AutoSetProgressHandler; typedef nsDataHashtable<nsUint64HashKey, bool> UniqueIndexTable; nsCOMPtr<nsIEventTarget> mOwningThread; const nsID mBackgroundChildLoggingId; const uint64_t mLoggingSerialNumber; nsresult mResultCode; private: Atomic<bool> mOperationMayProceed; bool mActorDestroyed; public: NS_DECL_ISUPPORTS_INHERITED bool IsOnOwningThread() const { MOZ_ASSERT(mOwningThread); bool current; return NS_SUCCEEDED(mOwningThread->IsOnCurrentThread(¤t)) && current; } void AssertIsOnOwningThread() const { MOZ_ASSERT(IsOnBackgroundThread()); MOZ_ASSERT(IsOnOwningThread()); } void NoteActorDestroyed() { AssertIsOnOwningThread(); mActorDestroyed = true; 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 aErrorCode) { MOZ_ASSERT(NS_SUCCEEDED(mResultCode)); MOZ_ASSERT(NS_FAILED(aErrorCode)); mResultCode = aErrorCode; } protected: DatabaseOperationBase(const nsID& aBackgroundChildLoggingId, uint64_t aLoggingSerialNumber) : mOwningThread(NS_GetCurrentThread()) , mBackgroundChildLoggingId(aBackgroundChildLoggingId) , mLoggingSerialNumber(aLoggingSerialNumber) , mResultCode(NS_OK) , mOperationMayProceed(true) , mActorDestroyed(false) { AssertIsOnOwningThread(); } virtual ~DatabaseOperationBase() { MOZ_ASSERT(mActorDestroyed); } static void GetBindingClauseForKeyRange(const SerializedKeyRange& aKeyRange, const nsACString& aKeyColumnName, nsAutoCString& aBindingClause); static uint64_t ReinterpretDoubleAsUInt64(double aDouble); static nsresult GetStructuredCloneReadInfoFromStatement(mozIStorageStatement* aStatement, uint32_t aDataIndex, uint32_t aFileIdsIndex, FileManager* aFileManager, StructuredCloneReadInfo* aInfo) { return GetStructuredCloneReadInfoFromSource(aStatement, aDataIndex, aFileIdsIndex, aFileManager, aInfo); } static nsresult GetStructuredCloneReadInfoFromValueArray(mozIStorageValueArray* aValues, uint32_t aDataIndex, uint32_t aFileIdsIndex, FileManager* aFileManager, StructuredCloneReadInfo* aInfo) { return GetStructuredCloneReadInfoFromSource(aValues, aDataIndex, aFileIdsIndex, aFileManager, aInfo); } static nsresult BindKeyRangeToStatement(const SerializedKeyRange& aKeyRange, mozIStorageStatement* aStatement); static nsresult BindKeyRangeToStatement(const SerializedKeyRange& aKeyRange, mozIStorageStatement* aStatement, const nsCString& aLocale); static void AppendConditionClause(const nsACString& aColumnName, const nsACString& aArgName, bool aLessThan, bool aEquals, nsAutoCString& aResult); static nsresult GetUniqueIndexTableForObjectStore( TransactionBase* aTransaction, int64_t aObjectStoreId, Maybe<UniqueIndexTable>& aMaybeUniqueIndexTable); static nsresult IndexDataValuesFromUpdateInfos(const nsTArray<IndexUpdateInfo>& aUpdateInfos, const UniqueIndexTable& aUniqueIndexTable, nsTArray<IndexDataValue>& aIndexValues); static nsresult InsertIndexTableRows(DatabaseConnection* aConnection, const int64_t aObjectStoreId, const Key& aObjectStoreKey, const FallibleTArray<IndexDataValue>& aIndexValues); static nsresult DeleteIndexDataTableRows(DatabaseConnection* aConnection, const Key& aObjectStoreKey, const FallibleTArray<IndexDataValue>& aIndexValues); static nsresult DeleteObjectStoreDataTableRowsWithIndexes(DatabaseConnection* aConnection, const int64_t aObjectStoreId, const OptionalKeyRange& aKeyRange); static nsresult UpdateIndexValues(DatabaseConnection* aConnection, const int64_t aObjectStoreId, const Key& aObjectStoreKey, const FallibleTArray<IndexDataValue>& aIndexValues); static nsresult ObjectStoreHasIndexes(DatabaseConnection* aConnection, const int64_t aObjectStoreId, bool* aHasIndexes); private: template <typename T> static nsresult GetStructuredCloneReadInfoFromSource(T* aSource, uint32_t aDataIndex, uint32_t aFileIdsIndex, FileManager* aFileManager, StructuredCloneReadInfo* aInfo); static nsresult GetStructuredCloneReadInfoFromBlob(const uint8_t* aBlobData, uint32_t aBlobDataLength, FileManager* aFileManager, const nsAString& aFileIds, StructuredCloneReadInfo* aInfo); static nsresult GetStructuredCloneReadInfoFromExternalBlob(uint64_t aIntData, FileManager* aFileManager, const nsAString& aFileIds, StructuredCloneReadInfo* aInfo); // Not to be overridden by subclasses. NS_DECL_MOZISTORAGEPROGRESSHANDLER }; class MOZ_STACK_CLASS DatabaseOperationBase::AutoSetProgressHandler final { mozIStorageConnection* mConnection; #ifdef DEBUG DatabaseOperationBase* mDEBUGDatabaseOp; #endif public: AutoSetProgressHandler(); ~AutoSetProgressHandler(); nsresult Register(mozIStorageConnection* aConnection, DatabaseOperationBase* aDatabaseOp); }; class TransactionDatabaseOperationBase : public DatabaseOperationBase { enum class InternalState { Initial, DatabaseWork, SendingPreprocess, WaitingForContinue, SendingResults, Completed }; RefPtr<TransactionBase> mTransaction; const int64_t mTransactionLoggingSerialNumber; InternalState mInternalState; const bool mTransactionIsAborted; public: void AssertIsOnConnectionThread() const #ifdef DEBUG ; #else { } #endif uint64_t StartOnConnectionPool(const nsID& aBackgroundChildLoggingId, const nsACString& aDatabaseId, int64_t aLoggingSerialNumber, const nsTArray<nsString>& aObjectStoreNames, bool aIsWriteTransaction); void DispatchToConnectionPool(); TransactionBase* Transaction() const { MOZ_ASSERT(mTransaction); return mTransaction; } void NoteContinueReceived(); // 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(TransactionBase* aTransaction); TransactionDatabaseOperationBase(TransactionBase* aTransaction, uint64_t aLoggingSerialNumber); virtual ~TransactionDatabaseOperationBase(); 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; 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 { RefPtr<DatabaseLoggingInfo> mLoggingInfo; #ifdef DEBUG bool mActorDestroyed; #endif public: static already_AddRefed<Factory> Create(const LoggingInfo& aLoggingInfo); DatabaseLoggingInfo* GetLoggingInfo() const { AssertIsOnBackgroundThread(); MOZ_ASSERT(mLoggingInfo); return mLoggingInfo; } NS_INLINE_DECL_THREADSAFE_REFCOUNTING(mozilla::dom::indexedDB::Factory) private: // Only constructed in Create(). explicit Factory(already_AddRefed<DatabaseLoggingInfo> aLoggingInfo); // Reference counted. ~Factory(); // IPDL methods are only called by IPDL. virtual void ActorDestroy(ActorDestroyReason aWhy) override; virtual bool RecvDeleteMe() override; virtual bool RecvIncrementLoggingRequestSerialNumber() override; virtual PBackgroundIDBFactoryRequestParent* AllocPBackgroundIDBFactoryRequestParent(const FactoryRequestParams& aParams) override; virtual bool RecvPBackgroundIDBFactoryRequestConstructor( PBackgroundIDBFactoryRequestParent* aActor, const FactoryRequestParams& aParams) override; virtual bool DeallocPBackgroundIDBFactoryRequestParent( PBackgroundIDBFactoryRequestParent* aActor) override; virtual PBackgroundIDBDatabaseParent* AllocPBackgroundIDBDatabaseParent( const DatabaseSpec& aSpec, PBackgroundIDBFactoryRequestParent* aRequest) override; virtual bool DeallocPBackgroundIDBDatabaseParent(PBackgroundIDBDatabaseParent* aActor) override; }; class WaitForTransactionsHelper final : public Runnable { const nsCString mDatabaseId; nsCOMPtr<nsIRunnable> mCallback; enum class State { Initial = 0, WaitingForTransactions, WaitingForFileHandles, Complete } mState; public: WaitForTransactionsHelper(const nsCString& aDatabaseId, nsIRunnable* aCallback) : mDatabaseId(aDatabaseId) , mCallback(aCallback) , mState(State::Initial) { AssertIsOnBackgroundThread(); MOZ_ASSERT(!aDatabaseId.IsEmpty()); MOZ_ASSERT(aCallback); } void WaitForTransactions(); NS_DECL_ISUPPORTS_INHERITED private: ~WaitForTransactionsHelper() { MOZ_ASSERT(!mCallback); MOZ_ASSERT(mState == State::Complete); } void MaybeWaitForTransactions(); void MaybeWaitForFileHandles(); void CallCallback(); NS_DECL_NSIRUNNABLE }; class Database final : public PBackgroundIDBDatabaseParent { friend class VersionChangeTransaction; class StartTransactionOp; private: RefPtr<Factory> mFactory; RefPtr<FullDatabaseMetadata> mMetadata; RefPtr<FileManager> mFileManager; RefPtr<DirectoryLock> mDirectoryLock; nsTHashtable<nsPtrHashKey<TransactionBase>> mTransactions; nsTHashtable<nsPtrHashKey<MutableFile>> mMutableFiles; RefPtr<DatabaseConnection> mConnection; const PrincipalInfo mPrincipalInfo; const Maybe<ContentParentId> mOptionalContentParentId; const nsCString mGroup; const nsCString mOrigin; const nsCString mId; const nsString mFilePath; uint32_t mActiveMutableFileCount; const uint32_t mTelemetryId; const PersistenceType mPersistenceType; const bool mFileHandleDisabled; const bool mChromeWriteAccessAllowed; bool mClosed; bool mInvalidated; bool mActorWasAlive; bool mActorDestroyed; bool mMetadataCleanedUp; public: // Created by OpenDatabaseOp. Database(Factory* aFactory, const PrincipalInfo& aPrincipalInfo, const Maybe<ContentParentId>& aOptionalContentParentId, const nsACString& aGroup, const nsACString& aOrigin, uint32_t aTelemetryId, FullDatabaseMetadata* aMetadata, FileManager* aFileManager, already_AddRefed<DirectoryLock> aDirectoryLock, bool aFileHandleDisabled, bool aChromeWriteAccessAllowed); 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_INLINE_DECL_THREADSAFE_REFCOUNTING(mozilla::dom::indexedDB::Database) void Invalidate(); const PrincipalInfo& GetPrincipalInfo() const { return mPrincipalInfo; } bool IsOwnedByProcess(ContentParentId aContentParentId) const { return mOptionalContentParentId && mOptionalContentParentId.value() == aContentParentId; } const nsCString& Group() const { return mGroup; } const nsCString& Origin() const { return mOrigin; } const nsCString& Id() const { return mId; } uint32_t TelemetryId() const { return mTelemetryId; } PersistenceType Type() const { return mPersistenceType; } const nsString& FilePath() const { return mFilePath; } FileManager* GetFileManager() const { return mFileManager; } FullDatabaseMetadata* Metadata() const { MOZ_ASSERT(mMetadata); return mMetadata; } PBackgroundParent* GetBackgroundParent() const { AssertIsOnBackgroundThread(); MOZ_ASSERT(!IsActorDestroyed()); return Manager()->Manager(); } DatabaseLoggingInfo* GetLoggingInfo() const { AssertIsOnBackgroundThread(); MOZ_ASSERT(mFactory); return mFactory->GetLoggingInfo(); } void ReleaseTransactionThreadObjects(); void ReleaseBackgroundThreadObjects(); bool RegisterTransaction(TransactionBase* aTransaction); void UnregisterTransaction(TransactionBase* aTransaction); bool IsFileHandleDisabled() const { return mFileHandleDisabled; } bool RegisterMutableFile(MutableFile* aMutableFile); void UnregisterMutableFile(MutableFile* aMutableFile); void NoteActiveMutableFile(); void NoteInactiveMutableFile(); void SetActorAlive(); 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; } private: // Reference counted. ~Database() { MOZ_ASSERT(mClosed); MOZ_ASSERT_IF(mActorWasAlive, mActorDestroyed); } bool CloseInternal(); void MaybeCloseConnection(); void ConnectionClosedCallback(); void CleanupMetadata(); bool VerifyRequestParams(const DatabaseRequestParams& aParams) const; // IPDL methods are only called by IPDL. virtual void ActorDestroy(ActorDestroyReason aWhy) override; virtual PBackgroundIDBDatabaseFileParent* AllocPBackgroundIDBDatabaseFileParent(PBlobParent* aBlobParent) override; virtual bool DeallocPBackgroundIDBDatabaseFileParent( PBackgroundIDBDatabaseFileParent* aActor) override; virtual PBackgroundIDBDatabaseRequestParent* AllocPBackgroundIDBDatabaseRequestParent(const DatabaseRequestParams& aParams) override; virtual bool RecvPBackgroundIDBDatabaseRequestConstructor( PBackgroundIDBDatabaseRequestParent* aActor, const DatabaseRequestParams& aParams) override; virtual bool DeallocPBackgroundIDBDatabaseRequestParent( PBackgroundIDBDatabaseRequestParent* aActor) override; virtual PBackgroundIDBTransactionParent* AllocPBackgroundIDBTransactionParent( const nsTArray<nsString>& aObjectStoreNames, const Mode& aMode) override; virtual bool RecvPBackgroundIDBTransactionConstructor( PBackgroundIDBTransactionParent* aActor, InfallibleTArray<nsString>&& aObjectStoreNames, const Mode& aMode) override; virtual bool DeallocPBackgroundIDBTransactionParent( PBackgroundIDBTransactionParent* aActor) override; virtual PBackgroundIDBVersionChangeTransactionParent* AllocPBackgroundIDBVersionChangeTransactionParent( const uint64_t& aCurrentVersion, const uint64_t& aRequestedVersion, const int64_t& aNextObjectStoreId, const int64_t& aNextIndexId) override; virtual bool DeallocPBackgroundIDBVersionChangeTransactionParent( PBackgroundIDBVersionChangeTransactionParent* aActor) override; virtual PBackgroundMutableFileParent* AllocPBackgroundMutableFileParent(const nsString& aName, const nsString& aType) override; virtual bool DeallocPBackgroundMutableFileParent(PBackgroundMutableFileParent* aActor) override; virtual bool RecvDeleteMe() override; virtual bool RecvBlocked() override; virtual bool RecvClose() override; }; class Database::StartTransactionOp final : public TransactionDatabaseOperationBase { friend class Database; private: explicit StartTransactionOp(TransactionBase* aTransaction) : TransactionDatabaseOperationBase(aTransaction, /* aLoggingSerialNumber */ 0) { } ~StartTransactionOp() { } virtual void RunOnConnectionThread() override; virtual nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; virtual nsresult SendSuccessResult() override; virtual bool SendFailureResult(nsresult aResultCode) override; virtual void Cleanup() override; }; /** * 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 * FileInfo 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 (that are therefore !IsShareable()) * 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 true even when a new "KnownBlob" actor * must be created because the reference is occurring on a different thread than * the PBlob actor created when the blob was sent to the child.) 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 { friend class Database; // 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. RefPtr<BlobImpl> mBlobImpl; RefPtr<FileInfo> mFileInfo; public: NS_INLINE_DECL_THREADSAFE_REFCOUNTING(mozilla::dom::indexedDB::DatabaseFile); FileInfo* GetFileInfo() const { AssertIsOnBackgroundThread(); return mFileInfo; } /** * If mBlobImpl is non-null (implying the contents of this file have not yet * been written to disk), then return an input stream that is guaranteed to * block on reads and never return NS_BASE_STREAM_WOULD_BLOCK. If mBlobImpl * is null (because the contents have been written to disk), returns null. * * Because this method does I/O, it should only be called on a database I/O * thread, not on PBackground. Note that we actually open the stream on this * thread, where previously it was opened on PBackground. This is safe and * equally efficient because blob implementations are thread-safe and blobs in * the parent are fully populated. (This would not be efficient in the child * where the open request would have to be dispatched back to the PBackground * thread because of the need to potentially interact with actors.) * * We enforce this guarantee by wrapping the stream in a blocking pipe unless * either is true: * - The stream is already a blocking stream. (AKA it's not non-blocking.) * This is the case for nsFileStreamBase-derived implementations and * appropriately configured nsPipes (but not PSendStream-generated pipes). * - The stream already contains the entire contents of the Blob. For * example, nsStringInputStreams report as non-blocking, but also have their * entire contents synchronously available, so they will never return * NS_BASE_STREAM_WOULD_BLOCK. There is no need to wrap these in a pipe. * (It's also very common for SendStream-based Blobs to have their contents * entirely streamed into the parent process by the time this call is * issued.) * * This additional logic is necessary because our database operations all * are written in such a way that the operation is assumed to have completed * when they yield control-flow, and: * - When memory-backed blobs cross a certain threshold (1MiB at the time of * writing), they will be sent up from the child via PSendStream in chunks * to a non-blocking pipe that will return NS_BASE_STREAM_WOULD_BLOCK. * - Other Blob types could potentially be non-blocking. (We're not making * any assumptions.) */ already_AddRefed<nsIInputStream> GetBlockingInputStream(ErrorResult &rv) const; /** * To be called upon successful copying of the stream GetBlockingInputStream() * returned so that we won't try and redundantly write the file to disk in the * future. This is a separate step from GetBlockingInputStream() 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()); mBlobImpl = nullptr; } private: // Called when sending to the child. explicit DatabaseFile(FileInfo* aFileInfo) : mFileInfo(aFileInfo) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aFileInfo); } // Called when receiving from the child. DatabaseFile(BlobImpl* aBlobImpl, FileInfo* aFileInfo) : mBlobImpl(aBlobImpl) , mFileInfo(aFileInfo) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aBlobImpl); MOZ_ASSERT(aFileInfo); } ~DatabaseFile() { } virtual void ActorDestroy(ActorDestroyReason aWhy) override { AssertIsOnBackgroundThread(); } }; already_AddRefed<nsIInputStream> DatabaseFile::GetBlockingInputStream(ErrorResult &rv) const { // We should only be called from our DB connection thread, not the background // thread. MOZ_ASSERT(!IsOnBackgroundThread()); if (!mBlobImpl) { return nullptr; } nsCOMPtr<nsIInputStream> inputStream; mBlobImpl->GetInternalStream(getter_AddRefs(inputStream), rv); if (rv.Failed()) { return nullptr; } // If it's non-blocking we may need a pipe. bool pipeNeeded; rv = inputStream->IsNonBlocking(&pipeNeeded); if (rv.Failed()) { return nullptr; } // We don't need a pipe if all the bytes might already be available. if (pipeNeeded) { uint64_t available; rv = inputStream->Available(&available); if (rv.Failed()) { return nullptr; } uint64_t blobSize = mBlobImpl->GetSize(rv); if (rv.Failed()) { return nullptr; } if (available == blobSize) { pipeNeeded = false; } } if (pipeNeeded) { nsCOMPtr<nsIEventTarget> target = do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); if (!target) { rv.Throw(NS_ERROR_UNEXPECTED); return nullptr; } nsCOMPtr<nsIInputStream> pipeInputStream; nsCOMPtr<nsIOutputStream> pipeOutputStream; rv = NS_NewPipe( getter_AddRefs(pipeInputStream), getter_AddRefs(pipeOutputStream), 0, 0, // default buffering is fine; false, // we absolutely want a blocking input stream true); // we don't need the writer to block if (rv.Failed()) { return nullptr; } rv = NS_AsyncCopy(inputStream, pipeOutputStream, target); if (rv.Failed()) { return nullptr; } inputStream = pipeInputStream; } return inputStream.forget(); } class TransactionBase { friend class Cursor; class CommitOp; protected: typedef IDBTransaction::Mode Mode; private: RefPtr<Database> mDatabase; nsTArray<RefPtr<FullObjectStoreMetadata>> mModifiedAutoIncrementObjectStoreMetadataArray; uint64_t mTransactionId; const nsCString mDatabaseId; const int64_t mLoggingSerialNumber; uint64_t mActiveRequestCount; Atomic<bool> mInvalidatedOnAnyThread; const Mode mMode; bool mHasBeenActive; bool mHasBeenActiveOnConnectionThread; bool mActorDestroyed; bool mInvalidated; protected: nsresult mResultCode; bool mCommitOrAbortReceived; bool mCommittedOrAborted; bool mForceAborted; 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 SetActive(uint64_t aTransactionId) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aTransactionId); mTransactionId = aTransactionId; mHasBeenActive = true; } void SetActiveOnConnectionThread() { AssertIsOnConnectionThread(); mHasBeenActiveOnConnectionThread = true; } NS_INLINE_DECL_THREADSAFE_REFCOUNTING( mozilla::dom::indexedDB::TransactionBase) void Abort(nsresult aResultCode, bool aForce); uint64_t TransactionId() const { return mTransactionId; } const nsCString& DatabaseId() const { return mDatabaseId; } Mode GetMode() const { return mMode; } Database* GetDatabase() const { MOZ_ASSERT(mDatabase); return mDatabase; } DatabaseLoggingInfo* GetLoggingInfo() const { AssertIsOnBackgroundThread(); MOZ_ASSERT(mDatabase); return mDatabase->GetLoggingInfo(); } int64_t LoggingSerialNumber() const { return mLoggingSerialNumber; } bool IsAborted() const { AssertIsOnBackgroundThread(); return NS_FAILED(mResultCode); } already_AddRefed<FullObjectStoreMetadata> GetMetadataForObjectStoreId(int64_t aObjectStoreId) const; already_AddRefed<FullIndexMetadata> GetMetadataForIndexId(FullObjectStoreMetadata* const aObjectStoreMetadata, int64_t aIndexId) const; PBackgroundParent* GetBackgroundParent() const { AssertIsOnBackgroundThread(); MOZ_ASSERT(!IsActorDestroyed()); return GetDatabase()->GetBackgroundParent(); } void NoteModifiedAutoIncrementObjectStore(FullObjectStoreMetadata* aMetadata); void ForgetModifiedAutoIncrementObjectStore(FullObjectStoreMetadata* aMetadata); void NoteActiveRequest(); void NoteFinishedRequest(); void Invalidate(); protected: TransactionBase(Database* aDatabase, Mode aMode); virtual ~TransactionBase(); void NoteActorDestroyed() { AssertIsOnBackgroundThread(); MOZ_ASSERT(!mActorDestroyed); mActorDestroyed = true; } #ifdef DEBUG // Only called by VersionChangeTransaction. void FakeActorDestroyed() { mActorDestroyed = true; } #endif bool RecvCommit(); bool RecvAbort(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(const RequestParams& aParams, bool aTrustParams); bool StartRequest(PBackgroundIDBRequestParent* aActor); bool DeallocRequest(PBackgroundIDBRequestParent* aActor); PBackgroundIDBCursorParent* AllocCursor(const OpenCursorParams& aParams, bool aTrustParams); bool StartCursor(PBackgroundIDBCursorParent* aActor, const OpenCursorParams& aParams); bool DeallocCursor(PBackgroundIDBCursorParent* aActor); virtual void UpdateMetadata(nsresult aResult) { } virtual void SendCompleteNotification(nsresult aResult) = 0; private: bool VerifyRequestParams(const RequestParams& aParams) const; bool VerifyRequestParams(const SerializedKeyRange& aKeyRange) const; bool VerifyRequestParams(const ObjectStoreAddPutParams& aParams) const; bool VerifyRequestParams(const OptionalKeyRange& aKeyRange) const; void CommitOrAbort(); }; class TransactionBase::CommitOp final : public DatabaseOperationBase , public ConnectionPool::FinishCallback { friend class TransactionBase; RefPtr<TransactionBase> mTransaction; nsresult mResultCode; private: CommitOp(TransactionBase* aTransaction, nsresult aResultCode); ~CommitOp() { } // 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 virtual void TransactionFinishedBeforeUnblock() override; virtual void TransactionFinishedAfterUnblock() override; public: NS_DECL_ISUPPORTS_INHERITED }; class NormalTransaction final : public TransactionBase , public PBackgroundIDBTransactionParent { friend class Database; nsTArray<RefPtr<FullObjectStoreMetadata>> mObjectStores; private: // This constructor is only called by Database. NormalTransaction(Database* aDatabase, TransactionBase::Mode aMode, nsTArray<RefPtr<FullObjectStoreMetadata>>& aObjectStores); // Reference counted. ~NormalTransaction() { } bool IsSameProcessActor(); // Only called by TransactionBase. virtual void SendCompleteNotification(nsresult aResult) override; // IPDL methods are only called by IPDL. virtual void ActorDestroy(ActorDestroyReason aWhy) override; virtual bool RecvDeleteMe() override; virtual bool RecvCommit() override; virtual bool RecvAbort(const nsresult& aResultCode) override; virtual PBackgroundIDBRequestParent* AllocPBackgroundIDBRequestParent(const RequestParams& aParams) override; virtual bool RecvPBackgroundIDBRequestConstructor(PBackgroundIDBRequestParent* aActor, const RequestParams& aParams) override; virtual bool DeallocPBackgroundIDBRequestParent(PBackgroundIDBRequestParent* aActor) override; virtual PBackgroundIDBCursorParent* AllocPBackgroundIDBCursorParent(const OpenCursorParams& aParams) override; virtual bool RecvPBackgroundIDBCursorConstructor(PBackgroundIDBCursorParent* aActor, const OpenCursorParams& aParams) override; virtual bool DeallocPBackgroundIDBCursorParent(PBackgroundIDBCursorParent* aActor) override; }; class VersionChangeTransaction final : public TransactionBase , public PBackgroundIDBVersionChangeTransactionParent { friend class OpenDatabaseOp; RefPtr<OpenDatabaseOp> mOpenDatabaseOp; RefPtr<FullDatabaseMetadata> mOldMetadata; bool mActorWasAlive; private: // Only called by OpenDatabaseOp. explicit VersionChangeTransaction(OpenDatabaseOp* aOpenDatabaseOp); // Reference counted. ~VersionChangeTransaction(); bool IsSameProcessActor(); // Only called by OpenDatabaseOp. bool CopyDatabaseMetadata(); void SetActorAlive(); // Only called by TransactionBase. virtual void UpdateMetadata(nsresult aResult) override; // Only called by TransactionBase. virtual void SendCompleteNotification(nsresult aResult) override; // IPDL methods are only called by IPDL. virtual void ActorDestroy(ActorDestroyReason aWhy) override; virtual bool RecvDeleteMe() override; virtual bool RecvCommit() override; virtual bool RecvAbort(const nsresult& aResultCode) override; virtual bool RecvCreateObjectStore(const ObjectStoreMetadata& aMetadata) override; virtual bool RecvDeleteObjectStore(const int64_t& aObjectStoreId) override; virtual bool RecvRenameObjectStore(const int64_t& aObjectStoreId, const nsString& aName) override; virtual bool RecvCreateIndex(const int64_t& aObjectStoreId, const IndexMetadata& aMetadata) override; virtual bool RecvDeleteIndex(const int64_t& aObjectStoreId, const int64_t& aIndexId) override; virtual bool RecvRenameIndex(const int64_t& aObjectStoreId, const int64_t& aIndexId, const nsString& aName) override; virtual PBackgroundIDBRequestParent* AllocPBackgroundIDBRequestParent(const RequestParams& aParams) override; virtual bool RecvPBackgroundIDBRequestConstructor(PBackgroundIDBRequestParent* aActor, const RequestParams& aParams) override; virtual bool DeallocPBackgroundIDBRequestParent(PBackgroundIDBRequestParent* aActor) override; virtual PBackgroundIDBCursorParent* AllocPBackgroundIDBCursorParent(const OpenCursorParams& aParams) override; virtual bool RecvPBackgroundIDBCursorConstructor(PBackgroundIDBCursorParent* aActor, const OpenCursorParams& aParams) override; virtual bool DeallocPBackgroundIDBCursorParent(PBackgroundIDBCursorParent* aActor) override; }; class MutableFile : public BackgroundMutableFileParentBase { RefPtr<Database> mDatabase; RefPtr<FileInfo> mFileInfo; public: static already_AddRefed<MutableFile> Create(nsIFile* aFile, Database* aDatabase, FileInfo* aFileInfo); Database* GetDatabase() const { AssertIsOnBackgroundThread(); MOZ_ASSERT(mDatabase); return mDatabase; } FileInfo* GetFileInfo() const { AssertIsOnBackgroundThread(); MOZ_ASSERT(mFileInfo); return mFileInfo; } virtual void NoteActiveState() override; virtual void NoteInactiveState() override; virtual PBackgroundParent* GetBackgroundParent() const override; virtual already_AddRefed<nsISupports> CreateStream(bool aReadOnly) override; virtual already_AddRefed<BlobImpl> CreateBlobImpl() override; private: MutableFile(nsIFile* aFile, Database* aDatabase, FileInfo* aFileInfo); ~MutableFile(); virtual PBackgroundFileHandleParent* AllocPBackgroundFileHandleParent(const FileMode& aMode) override; virtual bool RecvPBackgroundFileHandleConstructor(PBackgroundFileHandleParent* aActor, const FileMode& aMode) override; virtual bool RecvGetFileId(int64_t* aFileId) override; }; class FactoryOp : public DatabaseOperationBase , public OpenDirectoryListener , public PBackgroundIDBFactoryRequestParent { public: struct MaybeBlockedDatabaseInfo; 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, // Sending a permission challenge message to the child on the PBackground // thread. Next step is PermissionRetry. PermissionChallenge, // Retrying permission check after a challenge on the main thread. Next step // is either SendingResults if permission is denied or FinishOpen // if permission is granted. PermissionRetry, // Opening directory or initializing quota manager on the PBackground // thread. Next step is either DirectoryOpenPending if quota manager is // already initialized or QuotaManagerPending if quota manager needs to be // initialized. FinishOpen, // Waiting for quota manager initialization to complete on the PBackground // thread. Next step is either SendingResults if initialization failed or // DirectoryOpenPending if initialization succeeded. QuotaManagerPending, // 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! RefPtr<Factory> mFactory; // Must be released on the main thread! RefPtr<ContentParent> mContentParent; // Must be released on the main thread! RefPtr<DirectoryLock> mDirectoryLock; RefPtr<FactoryOp> mDelayedOp; nsTArray<MaybeBlockedDatabaseInfo> mMaybeBlockedDatabases; const CommonFactoryRequestParams mCommonParams; nsCString mSuffix; nsCString mGroup; nsCString mOrigin; nsCString mDatabaseId; nsString mDatabaseFilePath; State mState; bool mIsApp; bool mEnforcingQuota; const bool mDeleting; bool mBlockedDatabaseOpen; bool mChromeWriteAccessAllowed; bool mFileHandleDisabled; public: void NoteDatabaseBlocked(Database* aDatabase); virtual void NoteDatabaseClosed(Database* aDatabase) = 0; #ifdef DEBUG bool HasBlockedDatabases() const { return !mMaybeBlockedDatabases.IsEmpty(); } #endif const nsString& DatabaseFilePath() const { return mDatabaseFilePath; } protected: FactoryOp(Factory* aFactory, already_AddRefed<ContentParent> aContentParent, const CommonFactoryRequestParams& aCommonParams, bool aDeleting); virtual ~FactoryOp() { // 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 ChallengePermission(); nsresult RetryCheckPermission(); nsresult DirectoryOpen(); nsresult SendToIOThread(); void WaitForTransactions(); void FinishSendResults(); nsresult SendVersionChangeMessages(DatabaseActorInfo* aDatabaseActorInfo, Database* aOpeningDatabase, uint64_t aOldVersion, const NullableVersion& aNewVersion); // Methods that subclasses must implement. virtual nsresult DatabaseOpen() = 0; virtual nsresult DoDatabaseWork() = 0; virtual nsresult BeginVersionChange() = 0; virtual nsresult DispatchToWorkThread() = 0; // Should only be called by Run(). virtual void SendResults() = 0; NS_DECL_ISUPPORTS_INHERITED // Common nsIRunnable implementation that subclasses may not override. NS_IMETHOD Run() final; // OpenDirectoryListener overrides. virtual void DirectoryLockAcquired(DirectoryLock* aLock) override; virtual void DirectoryLockFailed() override; // IPDL methods. virtual void ActorDestroy(ActorDestroyReason aWhy) override; virtual bool RecvPermissionRetry() override; virtual void SendBlockedNotification() = 0; private: nsresult CheckPermission(ContentParent* aContentParent, PermissionRequestBase::PermissionValue* aPermission); static bool CheckAtLeastOneAppHasPermission(ContentParent* aContentParent, const nsACString& aPermissionString); nsresult FinishOpen(); nsresult QuotaManagerOpen(); nsresult OpenDirectory(); // Test whether this FactoryOp needs to wait for the given op. bool MustWaitFor(const FactoryOp& aExistingOp); }; struct FactoryOp::MaybeBlockedDatabaseInfo final { RefPtr<Database> mDatabase; bool mBlocked; MOZ_IMPLICIT MaybeBlockedDatabaseInfo(Database* aDatabase) : mDatabase(aDatabase) , mBlocked(false) { MOZ_ASSERT(aDatabase); MOZ_COUNT_CTOR(FactoryOp::MaybeBlockedDatabaseInfo); } ~MaybeBlockedDatabaseInfo() { MOZ_COUNT_DTOR(FactoryOp::MaybeBlockedDatabaseInfo); } bool operator==(const MaybeBlockedDatabaseInfo& aOther) const { return mDatabase == aOther.mDatabase; } bool operator<(const MaybeBlockedDatabaseInfo& aOther) const { return mDatabase < aOther.mDatabase; } Database* operator->() MOZ_NO_ADDREF_RELEASE_ON_RETURN { return mDatabase; } }; class OpenDatabaseOp final : public FactoryOp { friend class Database; friend class VersionChangeTransaction; class VersionChangeOp; Maybe<ContentParentId> mOptionalContentParentId; RefPtr<FullDatabaseMetadata> mMetadata; uint64_t mRequestedVersion; RefPtr<FileManager> mFileManager; RefPtr<Database> mDatabase; RefPtr<VersionChangeTransaction> 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(Factory* aFactory, already_AddRefed<ContentParent> aContentParent, const CommonFactoryRequestParams& aParams); bool IsOtherProcessActor() const { return mOptionalContentParentId.isSome(); } private: ~OpenDatabaseOp() { MOZ_ASSERT(!mVersionChangeOp); } nsresult LoadDatabaseInformation(mozIStorageConnection* aConnection); nsresult SendUpgradeNeeded(); void EnsureDatabaseActor(); nsresult EnsureDatabaseActorIsAlive(); void MetadataToSpec(DatabaseSpec& aSpec); void AssertMetadataConsistency(const FullDatabaseMetadata* aMetadata) #ifdef DEBUG ; #else { } #endif void ConnectionClosedCallback(); virtual void ActorDestroy(ActorDestroyReason aWhy) override; virtual nsresult DatabaseOpen() override; virtual nsresult DoDatabaseWork() override; virtual nsresult BeginVersionChange() override; virtual void NoteDatabaseClosed(Database* aDatabase) override; virtual void SendBlockedNotification() override; virtual nsresult DispatchToWorkThread() override; virtual void SendResults() override; #ifdef ENABLE_INTL_API static nsresult UpdateLocaleAwareIndex(mozIStorageConnection* aConnection, const IndexMetadata& aIndexMetadata, const nsCString& aLocale); #endif }; class OpenDatabaseOp::VersionChangeOp final : public TransactionDatabaseOperationBase { friend class OpenDatabaseOp; RefPtr<OpenDatabaseOp> mOpenDatabaseOp; const uint64_t mRequestedVersion; uint64_t mPreviousVersion; private: explicit VersionChangeOp(OpenDatabaseOp* aOpenDatabaseOp) : TransactionDatabaseOperationBase( aOpenDatabaseOp->mVersionChangeTransaction, aOpenDatabaseOp->LoggingSerialNumber()) , mOpenDatabaseOp(aOpenDatabaseOp) , mRequestedVersion(aOpenDatabaseOp->mRequestedVersion) , mPreviousVersion(aOpenDatabaseOp->mMetadata->mCommonMetadata.version()) { MOZ_ASSERT(aOpenDatabaseOp); MOZ_ASSERT(mRequestedVersion); } ~VersionChangeOp() { MOZ_ASSERT(!mOpenDatabaseOp); } virtual nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; virtual nsresult SendSuccessResult() override; virtual bool SendFailureResult(nsresult aResultCode) override; virtual void Cleanup() override; }; class DeleteDatabaseOp final : public FactoryOp { class VersionChangeOp; nsString mDatabaseDirectoryPath; nsString mDatabaseFilenameBase; uint64_t mPreviousVersion; public: DeleteDatabaseOp(Factory* aFactory, already_AddRefed<ContentParent> aContentParent, const CommonFactoryRequestParams& aParams) : FactoryOp(aFactory, Move(aContentParent), aParams, /* aDeleting */ true) , mPreviousVersion(0) { } private: ~DeleteDatabaseOp() { } void LoadPreviousVersion(nsIFile* aDatabaseFile); virtual nsresult DatabaseOpen() override; virtual nsresult DoDatabaseWork() override; virtual nsresult BeginVersionChange() override; virtual void NoteDatabaseClosed(Database* aDatabase) override; virtual void SendBlockedNotification() override; virtual nsresult DispatchToWorkThread() override; virtual void SendResults() override; }; class DeleteDatabaseOp::VersionChangeOp final : public DatabaseOperationBase { friend class DeleteDatabaseOp; RefPtr<DeleteDatabaseOp> mDeleteDatabaseOp; private: explicit VersionChangeOp(DeleteDatabaseOp* aDeleteDatabaseOp) : DatabaseOperationBase(aDeleteDatabaseOp->BackgroundChildLoggingId(), aDeleteDatabaseOp->LoggingSerialNumber()) , mDeleteDatabaseOp(aDeleteDatabaseOp) { MOZ_ASSERT(aDeleteDatabaseOp); MOZ_ASSERT(!aDeleteDatabaseOp->mDatabaseDirectoryPath.IsEmpty()); } ~VersionChangeOp() { } nsresult RunOnIOThread(); void RunOnOwningThread(); nsresult DeleteFile(nsIFile* aDirectory, const nsAString& aFilename, QuotaManager* aQuotaManager); NS_DECL_NSIRUNNABLE }; class DatabaseOp : public DatabaseOperationBase , public PBackgroundIDBDatabaseRequestParent { protected: RefPtr<Database> mDatabase; enum class State { // Just created on the PBackground thread, dispatched to the main thread. // Next step is DatabaseWork. Initial, // Waiting to do/doing work on the QuotaManager IO thread. Next step is // SendingResults. DatabaseWork, // Waiting to send/sending results on the PBackground thread. Next step is // Completed. SendingResults, // All done. Completed }; State mState; public: void RunImmediately() { MOZ_ASSERT(mState == State::Initial); Unused << this->Run(); } protected: DatabaseOp(Database* aDatabase); virtual ~DatabaseOp() { MOZ_ASSERT_IF(OperationMayProceed(), mState == State::Initial || mState == State::Completed); } nsresult SendToIOThread(); // Methods that subclasses must implement. virtual nsresult DoDatabaseWork() = 0; virtual void SendResults() = 0; // Common nsIRunnable implementation that subclasses may not override. NS_IMETHOD Run() final; // IPDL methods. virtual void ActorDestroy(ActorDestroyReason aWhy) override; }; class CreateFileOp final : public DatabaseOp { const CreateFileParams mParams; RefPtr<FileInfo> mFileInfo; public: CreateFileOp(Database* aDatabase, const DatabaseRequestParams& aParams); private: ~CreateFileOp() { } nsresult CreateMutableFile(MutableFile** aMutableFile); virtual nsresult DoDatabaseWork() override; virtual void SendResults() override; }; class VersionChangeTransactionOp : public TransactionDatabaseOperationBase { public: virtual void Cleanup() override; protected: explicit VersionChangeTransactionOp(VersionChangeTransaction* aTransaction) : TransactionDatabaseOperationBase(aTransaction) { } virtual ~VersionChangeTransactionOp() { } private: virtual nsresult SendSuccessResult() override; virtual bool SendFailureResult(nsresult aResultCode) override; }; class CreateObjectStoreOp final : public VersionChangeTransactionOp { friend class VersionChangeTransaction; const ObjectStoreMetadata mMetadata; private: // Only created by VersionChangeTransaction. CreateObjectStoreOp(VersionChangeTransaction* aTransaction, const ObjectStoreMetadata& aMetadata) : VersionChangeTransactionOp(aTransaction) , mMetadata(aMetadata) { MOZ_ASSERT(aMetadata.id()); } ~CreateObjectStoreOp() { } virtual nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; }; class DeleteObjectStoreOp final : public VersionChangeTransactionOp { friend class VersionChangeTransaction; const RefPtr<FullObjectStoreMetadata> mMetadata; const bool mIsLastObjectStore; private: // Only created by VersionChangeTransaction. DeleteObjectStoreOp(VersionChangeTransaction* aTransaction, FullObjectStoreMetadata* const aMetadata, const bool aIsLastObjectStore) : VersionChangeTransactionOp(aTransaction) , mMetadata(aMetadata) , mIsLastObjectStore(aIsLastObjectStore) { MOZ_ASSERT(aMetadata->mCommonMetadata.id()); } ~DeleteObjectStoreOp() { } virtual 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(VersionChangeTransaction* aTransaction, FullObjectStoreMetadata* const aMetadata) : VersionChangeTransactionOp(aTransaction) , mId(aMetadata->mCommonMetadata.id()) , mNewName(aMetadata->mCommonMetadata.name()) { MOZ_ASSERT(mId); } ~RenameObjectStoreOp() { } virtual nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; }; class CreateIndexOp final : public VersionChangeTransactionOp { friend class VersionChangeTransaction; class ThreadLocalJSContext; class UpdateIndexDataValuesFunction; static const unsigned int kBadThreadLocalIndex = static_cast<unsigned int>(-1); static unsigned int sThreadLocalIndex; const IndexMetadata mMetadata; Maybe<UniqueIndexTable> mMaybeUniqueIndexTable; RefPtr<FileManager> mFileManager; const nsCString mDatabaseId; const uint64_t mObjectStoreId; private: // Only created by VersionChangeTransaction. CreateIndexOp(VersionChangeTransaction* aTransaction, const int64_t aObjectStoreId, const IndexMetadata& aMetadata); ~CreateIndexOp() { } nsresult InsertDataFromObjectStore(DatabaseConnection* aConnection); nsresult InsertDataFromObjectStoreInternal(DatabaseConnection* aConnection); virtual bool Init(TransactionBase* aTransaction) override; virtual nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; }; class NormalJSContext { friend class nsAutoPtr<NormalJSContext>; static const JSClass sGlobalClass; static const uint32_t kContextHeapSize = 768 * 1024; JSContext* mContext; JSObject* mGlobal; public: static NormalJSContext* Create(); JSContext* Context() const { return mContext; } JSObject* Global() const { return mGlobal; } protected: NormalJSContext() : mContext(nullptr) , mGlobal(nullptr) { MOZ_COUNT_CTOR(NormalJSContext); } ~NormalJSContext() { MOZ_COUNT_DTOR(NormalJSContext); if (mContext) { JS_DestroyContext(mContext); } } bool Init(); }; class CreateIndexOp::ThreadLocalJSContext final : public NormalJSContext { friend class CreateIndexOp; friend class nsAutoPtr<ThreadLocalJSContext>; public: static ThreadLocalJSContext* GetOrCreate(); private: ThreadLocalJSContext() { MOZ_COUNT_CTOR(CreateIndexOp::ThreadLocalJSContext); } ~ThreadLocalJSContext() { MOZ_COUNT_DTOR(CreateIndexOp::ThreadLocalJSContext); } }; class CreateIndexOp::UpdateIndexDataValuesFunction final : public mozIStorageFunction { RefPtr<CreateIndexOp> mOp; RefPtr<DatabaseConnection> mConnection; JSContext* mCx; public: UpdateIndexDataValuesFunction(CreateIndexOp* aOp, DatabaseConnection* aConnection, JSContext* aCx) : mOp(aOp) , mConnection(aConnection) , mCx(aCx) { MOZ_ASSERT(aOp); MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(aCx); } NS_DECL_ISUPPORTS private: ~UpdateIndexDataValuesFunction() { } NS_DECL_MOZISTORAGEFUNCTION }; class DeleteIndexOp final : public VersionChangeTransactionOp { friend class VersionChangeTransaction; const int64_t mObjectStoreId; const int64_t mIndexId; const bool mUnique; const bool mIsLastIndex; private: // Only created by VersionChangeTransaction. DeleteIndexOp(VersionChangeTransaction* aTransaction, const int64_t aObjectStoreId, const int64_t aIndexId, const bool aUnique, const bool aIsLastIndex); ~DeleteIndexOp() { } nsresult RemoveReferencesToIndex(DatabaseConnection* aConnection, const Key& aObjectDataKey, nsTArray<IndexDataValue>& aIndexValues); virtual nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; }; class RenameIndexOp final : public VersionChangeTransactionOp { friend class VersionChangeTransaction; const int64_t mObjectStoreId; const int64_t mIndexId; const nsString mNewName; private: // Only created by VersionChangeTransaction. RenameIndexOp(VersionChangeTransaction* aTransaction, FullIndexMetadata* const aMetadata, int64_t aObjectStoreId) : VersionChangeTransactionOp(aTransaction) , mObjectStoreId(aObjectStoreId) , mIndexId(aMetadata->mCommonMetadata.id()) , mNewName(aMetadata->mCommonMetadata.name()) { MOZ_ASSERT(mIndexId); } ~RenameIndexOp() { } virtual nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; }; class NormalTransactionOp : public TransactionDatabaseOperationBase , public PBackgroundIDBRequestParent { #ifdef DEBUG bool mResponseSent; #endif public: virtual void Cleanup() override; protected: explicit NormalTransactionOp(TransactionBase* aTransaction) : TransactionDatabaseOperationBase(aTransaction) #ifdef DEBUG , mResponseSent(false) #endif { } virtual ~NormalTransactionOp() { } // An overload of DatabaseOperationBase's function that can avoid doing extra // work on non-versionchange transactions. static nsresult ObjectStoreHasIndexes(NormalTransactionOp* aOp, DatabaseConnection* aConnection, const int64_t aObjectStoreId, const bool aMayHaveIndexes, bool* aHasIndexes); virtual nsresult GetPreprocessParams(PreprocessParams& aParams); // Subclasses use this override to set the IPDL response value. virtual void GetResponse(RequestResponse& aResponse) = 0; private: virtual nsresult SendPreprocessInfo() override; virtual nsresult SendSuccessResult() override; virtual bool SendFailureResult(nsresult aResultCode) override; // IPDL methods. virtual void ActorDestroy(ActorDestroyReason aWhy) override; virtual bool RecvContinue(const PreprocessResponse& aResponse) override; }; class ObjectStoreAddOrPutRequestOp final : public NormalTransactionOp { friend class TransactionBase; typedef mozilla::dom::quota::PersistenceType PersistenceType; struct StoredFileInfo; class SCInputStream; const ObjectStoreAddPutParams mParams; Maybe<UniqueIndexTable> mUniqueIndexTable; // This must be non-const so that we can update the mNextAutoIncrementId field // if we are modifying an autoIncrement objectStore. RefPtr<FullObjectStoreMetadata> mMetadata; FallibleTArray<StoredFileInfo> mStoredFileInfos; Key mResponse; const nsCString mGroup; const nsCString mOrigin; const PersistenceType mPersistenceType; const bool mOverwrite; bool mObjectStoreMayHaveIndexes; bool mDataOverThreshold; private: // Only created by TransactionBase. ObjectStoreAddOrPutRequestOp(TransactionBase* aTransaction, const RequestParams& aParams); ~ObjectStoreAddOrPutRequestOp() { } nsresult RemoveOldIndexDataValues(DatabaseConnection* aConnection); virtual bool Init(TransactionBase* aTransaction) override; virtual nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; virtual void GetResponse(RequestResponse& aResponse) override; virtual void Cleanup() override; }; struct ObjectStoreAddOrPutRequestOp::StoredFileInfo final { RefPtr<DatabaseFile> mFileActor; RefPtr<FileInfo> mFileInfo; // A non-Blob-backed inputstream to write to disk. If null, mFileActor may // still have a stream for us to write. nsCOMPtr<nsIInputStream> mInputStream; StructuredCloneFile::FileType mType; StoredFileInfo() : mType(StructuredCloneFile::eBlob) { AssertIsOnBackgroundThread(); MOZ_COUNT_CTOR(ObjectStoreAddOrPutRequestOp::StoredFileInfo); } ~StoredFileInfo() { AssertIsOnBackgroundThread(); MOZ_COUNT_DTOR(ObjectStoreAddOrPutRequestOp::StoredFileInfo); } void Serialize(nsString& aText) { MOZ_ASSERT(mFileInfo); const int64_t id = mFileInfo->Id(); switch (mType) { case StructuredCloneFile::eBlob: aText.AppendInt(id); break; case StructuredCloneFile::eMutableFile: aText.AppendInt(-id); break; case StructuredCloneFile::eStructuredClone: aText.Append('.'); aText.AppendInt(id); break; case StructuredCloneFile::eWasmBytecode: aText.Append('/'); aText.AppendInt(id); break; case StructuredCloneFile::eWasmCompiled: aText.Append('\\'); aText.AppendInt(id); break; default: MOZ_CRASH("Should never get here!"); } } }; class ObjectStoreAddOrPutRequestOp::SCInputStream final : public nsIInputStream { const JSStructuredCloneData& mData; JSStructuredCloneData::IterImpl mIter; public: explicit SCInputStream(const JSStructuredCloneData& aData) : mData(aData) , mIter(aData.Iter()) { } private: virtual ~SCInputStream() { } NS_DECL_THREADSAFE_ISUPPORTS NS_DECL_NSIINPUTSTREAM }; class ObjectStoreGetRequestOp final : public NormalTransactionOp { friend class TransactionBase; const uint32_t mObjectStoreId; RefPtr<Database> mDatabase; const OptionalKeyRange mOptionalKeyRange; AutoTArray<StructuredCloneReadInfo, 1> mResponse; PBackgroundParent* mBackgroundParent; uint32_t mPreprocessInfoCount; const uint32_t mLimit; const bool mGetAll; private: // Only created by TransactionBase. ObjectStoreGetRequestOp(TransactionBase* aTransaction, const RequestParams& aParams, bool aGetAll); ~ObjectStoreGetRequestOp() { } template <bool aForPreprocess, typename T> nsresult ConvertResponse(StructuredCloneReadInfo& aInfo, T& aResult); virtual nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; virtual bool HasPreprocessInfo() override; virtual nsresult GetPreprocessParams(PreprocessParams& aParams) override; virtual void GetResponse(RequestResponse& aResponse) override; }; class ObjectStoreGetKeyRequestOp final : public NormalTransactionOp { friend class TransactionBase; const uint32_t mObjectStoreId; const OptionalKeyRange mOptionalKeyRange; const uint32_t mLimit; const bool mGetAll; FallibleTArray<Key> mResponse; private: // Only created by TransactionBase. ObjectStoreGetKeyRequestOp(TransactionBase* aTransaction, const RequestParams& aParams, bool aGetAll); ~ObjectStoreGetKeyRequestOp() { } virtual nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; virtual void GetResponse(RequestResponse& aResponse) override; }; class ObjectStoreDeleteRequestOp final : public NormalTransactionOp { friend class TransactionBase; const ObjectStoreDeleteParams mParams; ObjectStoreDeleteResponse mResponse; bool mObjectStoreMayHaveIndexes; private: ObjectStoreDeleteRequestOp(TransactionBase* aTransaction, const ObjectStoreDeleteParams& aParams); ~ObjectStoreDeleteRequestOp() { } virtual nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; virtual void GetResponse(RequestResponse& aResponse) override { aResponse = Move(mResponse); } }; class ObjectStoreClearRequestOp final : public NormalTransactionOp { friend class TransactionBase; const ObjectStoreClearParams mParams; ObjectStoreClearResponse mResponse; bool mObjectStoreMayHaveIndexes; private: ObjectStoreClearRequestOp(TransactionBase* aTransaction, const ObjectStoreClearParams& aParams); ~ObjectStoreClearRequestOp() { } virtual nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; virtual void GetResponse(RequestResponse& aResponse) override { aResponse = Move(mResponse); } }; class ObjectStoreCountRequestOp final : public NormalTransactionOp { friend class TransactionBase; const ObjectStoreCountParams mParams; ObjectStoreCountResponse mResponse; private: ObjectStoreCountRequestOp(TransactionBase* aTransaction, const ObjectStoreCountParams& aParams) : NormalTransactionOp(aTransaction) , mParams(aParams) { } ~ObjectStoreCountRequestOp() { } virtual nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; virtual void GetResponse(RequestResponse& aResponse) override { aResponse = Move(mResponse); } }; class IndexRequestOpBase : public NormalTransactionOp { protected: const RefPtr<FullIndexMetadata> mMetadata; protected: IndexRequestOpBase(TransactionBase* aTransaction, const RequestParams& aParams) : NormalTransactionOp(aTransaction) , mMetadata(IndexMetadataForParams(aTransaction, aParams)) { } virtual ~IndexRequestOpBase() { } private: static already_AddRefed<FullIndexMetadata> IndexMetadataForParams(TransactionBase* aTransaction, const RequestParams& aParams); }; class IndexGetRequestOp final : public IndexRequestOpBase { friend class TransactionBase; RefPtr<Database> mDatabase; const OptionalKeyRange mOptionalKeyRange; AutoTArray<StructuredCloneReadInfo, 1> mResponse; PBackgroundParent* mBackgroundParent; const uint32_t mLimit; const bool mGetAll; private: // Only created by TransactionBase. IndexGetRequestOp(TransactionBase* aTransaction, const RequestParams& aParams, bool aGetAll); ~IndexGetRequestOp() { } virtual nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; virtual void GetResponse(RequestResponse& aResponse) override; }; class IndexGetKeyRequestOp final : public IndexRequestOpBase { friend class TransactionBase; const OptionalKeyRange mOptionalKeyRange; AutoTArray<Key, 1> mResponse; const uint32_t mLimit; const bool mGetAll; private: // Only created by TransactionBase. IndexGetKeyRequestOp(TransactionBase* aTransaction, const RequestParams& aParams, bool aGetAll); ~IndexGetKeyRequestOp() { } virtual nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; virtual void GetResponse(RequestResponse& aResponse) override; }; class IndexCountRequestOp final : public IndexRequestOpBase { friend class TransactionBase; const IndexCountParams mParams; IndexCountResponse mResponse; private: // Only created by TransactionBase. IndexCountRequestOp(TransactionBase* aTransaction, const RequestParams& aParams) : IndexRequestOpBase(aTransaction, aParams) , mParams(aParams.get_IndexCountParams()) { } ~IndexCountRequestOp() { } virtual nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; virtual void GetResponse(RequestResponse& aResponse) override { aResponse = Move(mResponse); } }; class Cursor final : public PBackgroundIDBCursorParent { friend class TransactionBase; class ContinueOp; class CursorOpBase; class OpenOp; public: typedef OpenCursorParams::Type Type; private: RefPtr<TransactionBase> mTransaction; RefPtr<Database> mDatabase; RefPtr<FileManager> mFileManager; PBackgroundParent* mBackgroundParent; // These should only be touched on the PBackground thread to check whether the // objectStore or index has been deleted. Holding these saves a hash lookup // for every call to continue()/advance(). RefPtr<FullObjectStoreMetadata> mObjectStoreMetadata; RefPtr<FullIndexMetadata> mIndexMetadata; const int64_t mObjectStoreId; const int64_t mIndexId; nsCString mContinueQuery; nsCString mContinueToQuery; nsCString mContinuePrimaryKeyQuery; nsCString mLocale; Key mKey; Key mObjectKey; Key mRangeKey; Key mSortKey; CursorOpBase* mCurrentlyRunningOp; const Type mType; const Direction mDirection; const bool mUniqueIndex; const bool mIsSameProcessActor; bool mActorDestroyed; public: NS_INLINE_DECL_THREADSAFE_REFCOUNTING(mozilla::dom::indexedDB::Cursor) private: // Only created by TransactionBase. Cursor(TransactionBase* aTransaction, Type aType, FullObjectStoreMetadata* aObjectStoreMetadata, FullIndexMetadata* aIndexMetadata, Direction aDirection); // Reference counted. ~Cursor() { MOZ_ASSERT(mActorDestroyed); } bool VerifyRequestParams(const CursorRequestParams& aParams) const; // Only called by TransactionBase. bool Start(const OpenCursorParams& aParams); void SendResponseInternal( CursorResponse& aResponse, const nsTArray<FallibleTArray<StructuredCloneFile>>& aFiles); // Must call SendResponseInternal! bool SendResponse(const CursorResponse& aResponse) = delete; // IPDL methods. virtual void ActorDestroy(ActorDestroyReason aWhy) override; virtual bool RecvDeleteMe() override; virtual bool RecvContinue(const CursorRequestParams& aParams) override; bool IsLocaleAware() const { return !mLocale.IsEmpty(); } }; class Cursor::CursorOpBase : public TransactionDatabaseOperationBase { protected: RefPtr<Cursor> mCursor; nsTArray<FallibleTArray<StructuredCloneFile>> mFiles; CursorResponse mResponse; #ifdef DEBUG bool mResponseSent; #endif protected: explicit CursorOpBase(Cursor* aCursor) : TransactionDatabaseOperationBase(aCursor->mTransaction) , mCursor(aCursor) #ifdef DEBUG , mResponseSent(false) #endif { AssertIsOnBackgroundThread(); MOZ_ASSERT(aCursor); } virtual ~CursorOpBase() { } virtual bool SendFailureResult(nsresult aResultCode) override; virtual void Cleanup() override; nsresult PopulateResponseFromStatement(DatabaseConnection::CachedStatement& aStmt, bool aInitializeResponse); }; class Cursor::OpenOp final : public Cursor::CursorOpBase { friend class Cursor; const OptionalKeyRange mOptionalKeyRange; private: // Only created by Cursor. OpenOp(Cursor* aCursor, const OptionalKeyRange& aOptionalKeyRange) : CursorOpBase(aCursor) , mOptionalKeyRange(aOptionalKeyRange) { } // Reference counted. ~OpenOp() { } void GetRangeKeyInfo(bool aLowerBound, Key* aKey, bool* aOpen); nsresult DoObjectStoreDatabaseWork(DatabaseConnection* aConnection); nsresult DoObjectStoreKeyDatabaseWork(DatabaseConnection* aConnection); nsresult DoIndexDatabaseWork(DatabaseConnection* aConnection); nsresult DoIndexKeyDatabaseWork(DatabaseConnection* aConnection); virtual nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; virtual nsresult SendSuccessResult() override; }; class Cursor::ContinueOp final : public Cursor::CursorOpBase { friend class Cursor; const CursorRequestParams mParams; private: // Only created by Cursor. ContinueOp(Cursor* aCursor, const CursorRequestParams& aParams) : CursorOpBase(aCursor) , mParams(aParams) { MOZ_ASSERT(aParams.type() != CursorRequestParams::T__None); } // Reference counted. ~ContinueOp() { } virtual nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; virtual nsresult SendSuccessResult() override; }; 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(); // IPDL methods are only called by IPDL. virtual void ActorDestroy(ActorDestroyReason aWhy) override; virtual bool RecvDeleteMe() override; virtual bool RecvGetFileReferences(const PersistenceType& aPersistenceType, const nsCString& aOrigin, const nsString& aDatabaseName, const int64_t& aFileId, int32_t* aRefCnt, int32_t* aDBRefCnt, int32_t* aSliceRefCnt, bool* aResult) override; }; class GetFileReferencesHelper final : public Runnable { PersistenceType mPersistenceType; nsCString mOrigin; nsString mDatabaseName; int64_t mFileId; mozilla::Mutex mMutex; mozilla::CondVar mCondVar; int32_t mMemRefCnt; int32_t mDBRefCnt; int32_t mSliceRefCnt; bool mResult; bool mWaiting; public: GetFileReferencesHelper(PersistenceType aPersistenceType, const nsACString& aOrigin, const nsAString& aDatabaseName, int64_t aFileId) : mPersistenceType(aPersistenceType) , mOrigin(aOrigin) , mDatabaseName(aDatabaseName) , mFileId(aFileId) , mMutex("GetFileReferencesHelper::mMutex") , mCondVar(mMutex, "GetFileReferencesHelper::mCondVar") , mMemRefCnt(-1) , mDBRefCnt(-1) , mSliceRefCnt(-1) , mResult(false) , mWaiting(true) { } nsresult DispatchAndReturnFileReferences(int32_t* aMemRefCnt, int32_t* aDBRefCnt, int32_t* aSliceRefCnt, bool* aResult); private: ~GetFileReferencesHelper() {} NS_DECL_NSIRUNNABLE }; class FlushPendingFileDeletionsRunnable final : public Runnable { private: ~FlushPendingFileDeletionsRunnable() { } NS_DECL_NSIRUNNABLE }; class PermissionRequestHelper final : public PermissionRequestBase , public PIndexedDBPermissionRequestParent { bool mActorDestroyed; public: PermissionRequestHelper(Element* aOwnerElement, nsIPrincipal* aPrincipal) : PermissionRequestBase(aOwnerElement, aPrincipal) , mActorDestroyed(false) { } protected: ~PermissionRequestHelper() { } private: virtual void OnPromptComplete(PermissionValue aPermissionValue) override; virtual void ActorDestroy(ActorDestroyReason aWhy) override; }; /******************************************************************************* * Other class declarations ******************************************************************************/ struct DatabaseActorInfo final { friend class nsAutoPtr<DatabaseActorInfo>; RefPtr<FullDatabaseMetadata> mMetadata; nsTArray<Database*> mLiveDatabases; RefPtr<FactoryOp> mWaitingFactoryOp; DatabaseActorInfo(FullDatabaseMetadata* aMetadata, Database* aDatabase) : mMetadata(aMetadata) { MOZ_ASSERT(aDatabase); 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::VERSION_CHANGE) { 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 BlobImplStoredFile final : public BlobImplFile { RefPtr<FileInfo> mFileInfo; const bool mSnapshot; public: BlobImplStoredFile(nsIFile* aFile, FileInfo* aFileInfo, bool aSnapshot) : BlobImplFile(aFile) , mFileInfo(aFileInfo) , mSnapshot(aSnapshot) { AssertIsOnBackgroundThread(); // Getting the content type is not currently supported off the main thread. // This isn't a problem here because: // // 1. The real content type is stored in the structured clone data and // that's all that the DOM will see. This blob's data will be updated // during RecvSetMysteryBlobInfo(). // 2. The nsExternalHelperAppService guesses the content type based only // on the file extension. Our stored files have no extension so the // current code path fails and sets the content type to the empty // string. // // So, this is a hack to keep the nsExternalHelperAppService out of the // picture entirely. Eventually we should probably fix this some other way. mContentType.Truncate(); mIsFile = false; } bool IsShareable(FileManager* aFileManager) const { AssertIsOnBackgroundThread(); return mFileInfo->Manager() == aFileManager && !mSnapshot; } FileInfo* GetFileInfo() const { AssertIsOnBackgroundThread(); return mFileInfo; } private: ~BlobImplStoredFile() { } NS_DECL_ISUPPORTS_INHERITED NS_DECLARE_STATIC_IID_ACCESSOR(BLOB_IMPL_STORED_FILE_IID) virtual int64_t GetFileId() override { MOZ_ASSERT(mFileInfo); return mFileInfo->Id(); } }; NS_DEFINE_STATIC_IID_ACCESSOR(BlobImplStoredFile, BLOB_IMPL_STORED_FILE_IID) class QuotaClient final : public mozilla::dom::quota::Client { static QuotaClient* sInstance; nsCOMPtr<nsIEventTarget> mBackgroundThread; nsTArray<RefPtr<Maintenance>> mMaintenanceQueue; RefPtr<Maintenance> mCurrentMaintenance; RefPtr<nsThreadPool> mMaintenanceThreadPool; bool mShutdownRequested; public: QuotaClient(); static QuotaClient* GetInstance() { AssertIsOnBackgroundThread(); return sInstance; } static bool IsShuttingDownOnBackgroundThread() { AssertIsOnBackgroundThread(); if (sInstance) { return sInstance->IsShuttingDown(); } return QuotaManager::IsShuttingDown(); } static bool IsShuttingDownOnNonBackgroundThread() { MOZ_ASSERT(!IsOnBackgroundThread()); return QuotaManager::IsShuttingDown(); } nsIEventTarget* BackgroundThread() const { MOZ_ASSERT(mBackgroundThread); return mBackgroundThread; } bool IsShuttingDown() const { AssertIsOnBackgroundThread(); return mShutdownRequested; } already_AddRefed<Maintenance> GetCurrentMaintenance() const { RefPtr<Maintenance> result = mCurrentMaintenance; return result.forget(); } void NoteFinishedMaintenance(Maintenance* aMaintenance) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aMaintenance); MOZ_ASSERT(mCurrentMaintenance == aMaintenance); mCurrentMaintenance = nullptr; ProcessMaintenanceQueue(); } nsThreadPool* GetOrCreateThreadPool(); NS_INLINE_DECL_THREADSAFE_REFCOUNTING(QuotaClient, override) virtual mozilla::dom::quota::Client::Type GetType() override; virtual nsresult InitOrigin(PersistenceType aPersistenceType, const nsACString& aGroup, const nsACString& aOrigin, const AtomicBool& aCanceled, UsageInfo* aUsageInfo) override; virtual nsresult GetUsageForOrigin(PersistenceType aPersistenceType, const nsACString& aGroup, const nsACString& aOrigin, const AtomicBool& aCanceled, UsageInfo* aUsageInfo) override; virtual void OnOriginClearCompleted(PersistenceType aPersistenceType, const nsACString& aOrigin) override; virtual void ReleaseIOThreadObjects() override; virtual void AbortOperations(const nsACString& aOrigin) override; virtual void AbortOperationsForProcess(ContentParentId aContentParentId) override; virtual void StartIdleMaintenance() override; virtual void StopIdleMaintenance() override; virtual void ShutdownWorkThreads() override; virtual void DidInitialize(QuotaManager* aQuotaManager) override; virtual void WillShutdown() override; private: ~QuotaClient(); nsresult GetDirectory(PersistenceType aPersistenceType, const nsACString& aOrigin, nsIFile** aDirectory); nsresult GetUsageForDirectoryInternal(nsIFile* aDirectory, const AtomicBool& aCanceled, UsageInfo* aUsageInfo, bool aDatabaseFiles); // Runs on the PBackground thread. Checks to see if there's a queued // Maintenance to run. void ProcessMaintenanceQueue(); }; class Maintenance final : public Runnable , public OpenDirectoryListener { struct 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<QuotaClient> mQuotaClient; PRTime mStartTime; RefPtr<DirectoryLock> mDirectoryLock; nsTArray<DirectoryInfo> mDirectoryInfos; nsDataHashtable<nsStringHashKey, DatabaseMaintenance*> mDatabaseMaintenances; nsresult mResultCode; Atomic<bool> mAborted; State mState; public: explicit Maintenance(QuotaClient* aQuotaClient) : 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() { AssertIsOnBackgroundThread(); mAborted = true; } void RegisterDatabaseMaintenance(DatabaseMaintenance* aDatabaseMaintenance); void UnregisterDatabaseMaintenance(DatabaseMaintenance* aDatabaseMaintenance); already_AddRefed<DatabaseMaintenance> GetDatabaseMaintenance(const nsAString& aDatabasePath) const { AssertIsOnBackgroundThread(); RefPtr<DatabaseMaintenance> result = mDatabaseMaintenances.Get(aDatabasePath); return result.forget(); } private: ~Maintenance() { 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_ISUPPORTS_INHERITED NS_DECL_NSIRUNNABLE // OpenDirectoryListener overrides. virtual void DirectoryLockAcquired(DirectoryLock* aLock) override; virtual void DirectoryLockFailed() override; }; struct Maintenance::DirectoryInfo final { const nsCString mGroup; const nsCString mOrigin; nsTArray<nsString> mDatabasePaths; const PersistenceType mPersistenceType; DirectoryInfo(PersistenceType aPersistenceType, const nsACString& aGroup, const nsACString& aOrigin, nsTArray<nsString>&& aDatabasePaths) : mGroup(aGroup) , mOrigin(aOrigin) , mDatabasePaths(Move(aDatabasePaths)) , mPersistenceType(aPersistenceType) { MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_INVALID); MOZ_ASSERT(!aGroup.IsEmpty()); MOZ_ASSERT(!aOrigin.IsEmpty()); #ifdef DEBUG MOZ_ASSERT(!mDatabasePaths.IsEmpty()); for (const nsString& databasePath : mDatabasePaths) { MOZ_ASSERT(!databasePath.IsEmpty()); } #endif MOZ_COUNT_CTOR(Maintenance::DirectoryInfo); } DirectoryInfo(DirectoryInfo&& aOther) : mGroup(Move(aOther.mGroup)) , mOrigin(Move(aOther.mOrigin)) , mDatabasePaths(Move(aOther.mDatabasePaths)) , mPersistenceType(Move(aOther.mPersistenceType)) { #ifdef DEBUG MOZ_ASSERT(!mDatabasePaths.IsEmpty()); for (const nsString& databasePath : mDatabasePaths) { MOZ_ASSERT(!databasePath.IsEmpty()); } #endif MOZ_COUNT_CTOR(Maintenance::DirectoryInfo); } ~DirectoryInfo() { MOZ_COUNT_DTOR(Maintenance::DirectoryInfo); } DirectoryInfo(const DirectoryInfo& aOther) = delete; }; 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; class AutoProgressHandler; enum class MaintenanceAction { Nothing = 0, IncrementalVacuum, FullVacuum }; RefPtr<Maintenance> mMaintenance; const nsCString mGroup; const nsCString mOrigin; const nsString mDatabasePath; nsCOMPtr<nsIRunnable> mCompleteCallback; const PersistenceType mPersistenceType; public: DatabaseMaintenance(Maintenance* aMaintenance, PersistenceType aPersistenceType, const nsCString& aGroup, const nsCString& aOrigin, const nsString& aDatabasePath) : mMaintenance(aMaintenance) , mGroup(aGroup) , mOrigin(aOrigin) , mDatabasePath(aDatabasePath) , mPersistenceType(aPersistenceType) { } const nsString& DatabasePath() const { return mDatabasePath; } void WaitForCompletion(nsIRunnable* aCallback) { AssertIsOnBackgroundThread(); MOZ_ASSERT(!mCompleteCallback); mCompleteCallback = aCallback; } private: ~DatabaseMaintenance() { } // 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(); NS_DECL_NSIRUNNABLE }; class MOZ_STACK_CLASS DatabaseMaintenance::AutoProgressHandler final : public mozIStorageProgressHandler { Maintenance* mMaintenance; mozIStorageConnection* mConnection; NS_DECL_OWNINGTHREAD #ifdef DEBUG // This class is stack-based so we never actually allow AddRef/Release to do // anything. But we need to know if any consumer *thinks* that they have a // reference to this object so we track the reference countin DEBUG builds. nsrefcnt mDEBUGRefCnt; #endif public: explicit AutoProgressHandler(Maintenance* aMaintenance) : mMaintenance(aMaintenance) , mConnection(nullptr) #ifdef DEBUG , mDEBUGRefCnt(0) #endif { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!IsOnBackgroundThread()); NS_ASSERT_OWNINGTHREAD(DatabaseMaintenance::AutoProgressHandler); MOZ_ASSERT(aMaintenance); } ~AutoProgressHandler() { NS_ASSERT_OWNINGTHREAD(DatabaseMaintenance::AutoProgressHandler); if (mConnection) { Unregister(); } MOZ_ASSERT(!mDEBUGRefCnt); } nsresult Register(mozIStorageConnection* aConnection); // We don't want the mRefCnt member but this class does not "inherit" // nsISupports. NS_DECL_ISUPPORTS_INHERITED private: void Unregister(); NS_DECL_MOZISTORAGEPROGRESSHANDLER // Not available for the heap! void* operator new(size_t) = delete; void* operator new[](size_t) = delete; void operator delete(void*) = delete; void operator delete[](void*) = delete; }; class IntString : public nsAutoString { public: explicit IntString(int64_t aInteger) { AppendInt(aInteger); } }; #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 ******************************************************************************/ class MOZ_STACK_CLASS FileHelper final { RefPtr<FileManager> mFileManager; nsCOMPtr<nsIFile> mFileDirectory; nsCOMPtr<nsIFile> mJournalDirectory; public: explicit FileHelper(FileManager* aFileManager) : mFileManager(aFileManager) { } nsresult Init(); already_AddRefed<nsIFile> GetFile(FileInfo* aFileInfo); already_AddRefed<nsIFile> GetCheckedFile(FileInfo* aFileInfo); already_AddRefed<nsIFile> GetJournalFile(FileInfo* aFileInfo); nsresult CreateFileFromStream(nsIFile* aFile, nsIFile* aJournalFile, nsIInputStream* aInputStream, bool aCompress); nsresult ReplaceFile(nsIFile* aFile, nsIFile* aNewFile, nsIFile* aNewJournalFile); nsresult RemoveFile(nsIFile* aFile, nsIFile* aJournalFile); already_AddRefed<FileInfo> GetNewFileInfo(); private: nsresult SyncCopy(nsIInputStream* aInputStream, nsIOutputStream* aOutputStream, char* aBuffer, uint32_t aBufferSize); }; /******************************************************************************* * Helper Functions ******************************************************************************/ bool TokenizerIgnoreNothing(char16_t /* aChar */) { return false; } nsresult DeserializeStructuredCloneFile(FileManager* aFileManager, const nsString& aText, StructuredCloneFile* aFile) { MOZ_ASSERT(!aText.IsEmpty()); MOZ_ASSERT(aFile); StructuredCloneFile::FileType type; switch (aText.First()) { case char16_t('-'): type = StructuredCloneFile::eMutableFile; break; case char16_t('.'): type = StructuredCloneFile::eStructuredClone; break; case char16_t('/'): type = StructuredCloneFile::eWasmBytecode; break; case char16_t('\\'): type = StructuredCloneFile::eWasmCompiled; break; default: type = StructuredCloneFile::eBlob; } nsresult rv; int32_t id; if (type == StructuredCloneFile::eBlob) { id = aText.ToInteger(&rv); } else { nsString text(Substring(aText, 1)); id = text.ToInteger(&rv); } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } RefPtr<FileInfo> fileInfo = aFileManager->GetFileInfo(id); MOZ_ASSERT(fileInfo); aFile->mFileInfo.swap(fileInfo); aFile->mType = type; return NS_OK; } nsresult CheckWasmModule(FileHelper* aFileHelper, StructuredCloneFile* aBytecodeFile, StructuredCloneFile* aCompiledFile) { MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(aFileHelper); MOZ_ASSERT(aBytecodeFile); MOZ_ASSERT(aCompiledFile); MOZ_ASSERT(aBytecodeFile->mType == StructuredCloneFile::eWasmBytecode); MOZ_ASSERT(aCompiledFile->mType == StructuredCloneFile::eWasmCompiled); nsCOMPtr<nsIFile> compiledFile = aFileHelper->GetCheckedFile(aCompiledFile->mFileInfo); if (NS_WARN_IF(!compiledFile)) { return NS_ERROR_FAILURE; } nsresult rv; bool match; { ScopedPRFileDesc compiledFileDesc; rv = compiledFile->OpenNSPRFileDesc(PR_RDONLY, 0644, &compiledFileDesc.rwget()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } JS::BuildIdCharVector buildId; bool ok = GetBuildId(&buildId); if (NS_WARN_IF(!ok)) { return NS_ERROR_FAILURE; } match = JS::CompiledWasmModuleAssumptionsMatch(compiledFileDesc, Move(buildId)); } if (match) { return NS_OK; } // Re-compile the module. It would be preferable to do this in the child // (content process) instead of here in the parent, but that would be way more // complex and without significant memory allocation or security benefits. // See the discussion starting from // https://bugzilla.mozilla.org/show_bug.cgi?id=1312808#c9 for more details. nsCOMPtr<nsIFile> bytecodeFile = aFileHelper->GetCheckedFile(aBytecodeFile->mFileInfo); if (NS_WARN_IF(!bytecodeFile)) { return NS_ERROR_FAILURE; } ScopedPRFileDesc bytecodeFileDesc; rv = bytecodeFile->OpenNSPRFileDesc(PR_RDONLY, 0644, &bytecodeFileDesc.rwget()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } JS::BuildIdCharVector buildId; bool ok = GetBuildId(&buildId); if (NS_WARN_IF(!ok)) { return NS_ERROR_FAILURE; } RefPtr<JS::WasmModule> module = JS::DeserializeWasmModule(bytecodeFileDesc, nullptr, Move(buildId), nullptr, 0, 0); if (NS_WARN_IF(!module)) { return NS_ERROR_FAILURE; } size_t compiledSize; module->serializedSize(nullptr, &compiledSize); UniquePtr<uint8_t[]> compiled(new (fallible) uint8_t[compiledSize]); if (NS_WARN_IF(!compiled)) { return NS_ERROR_OUT_OF_MEMORY; } module->serialize(nullptr, 0, compiled.get(), compiledSize); nsCOMPtr<nsIInputStream> inputStream; rv = NS_NewByteInputStream(getter_AddRefs(inputStream), reinterpret_cast<const char*>(compiled.get()), compiledSize); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } RefPtr<FileInfo> newFileInfo = aFileHelper->GetNewFileInfo(); nsCOMPtr<nsIFile> newFile = aFileHelper->GetFile(newFileInfo); if (NS_WARN_IF(!newFile)) { return NS_ERROR_FAILURE; } nsCOMPtr<nsIFile> newJournalFile = aFileHelper->GetJournalFile(newFileInfo); if (NS_WARN_IF(!newJournalFile)) { return NS_ERROR_FAILURE; } rv = aFileHelper->CreateFileFromStream(newFile, newJournalFile, inputStream, /* aCompress */ false); if (NS_WARN_IF(NS_FAILED(rv))) { nsresult rv2 = aFileHelper->RemoveFile(newFile, newJournalFile); if (NS_WARN_IF(NS_FAILED(rv2))) { return rv; } return rv; } rv = aFileHelper->ReplaceFile(compiledFile, newFile, newJournalFile); if (NS_WARN_IF(NS_FAILED(rv))) { nsresult rv2 = aFileHelper->RemoveFile(newFile, newJournalFile); if (NS_WARN_IF(NS_FAILED(rv2))) { return rv; } return rv; } return NS_OK; } nsresult DeserializeStructuredCloneFiles(FileManager* aFileManager, const nsAString& aText, nsTArray<StructuredCloneFile>& aResult, bool* aHasPreprocessInfo) { MOZ_ASSERT(!IsOnBackgroundThread()); nsCharSeparatedTokenizerTemplate<TokenizerIgnoreNothing> tokenizer(aText, ' '); nsAutoString token; nsresult rv; Maybe<FileHelper> fileHelper; while (tokenizer.hasMoreTokens()) { token = tokenizer.nextToken(); MOZ_ASSERT(!token.IsEmpty()); StructuredCloneFile* file = aResult.AppendElement(); rv = DeserializeStructuredCloneFile(aFileManager, token, file); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (!aHasPreprocessInfo) { continue; } if (file->mType == StructuredCloneFile::eWasmBytecode) { *aHasPreprocessInfo = true; } else if (file->mType == StructuredCloneFile::eWasmCompiled) { if (fileHelper.isNothing()) { fileHelper.emplace(aFileManager); rv = fileHelper->Init(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } MOZ_ASSERT(aResult.Length() > 1); MOZ_ASSERT(aResult[aResult.Length() - 2].mType == StructuredCloneFile::eWasmBytecode); StructuredCloneFile* previousFile = &aResult[aResult.Length() - 2]; rv = CheckWasmModule(fileHelper.ptr(), previousFile, file); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } *aHasPreprocessInfo = true; } } return NS_OK; } bool GetDatabaseBaseFilename(const nsAString& aFilename, nsDependentSubstring& aDatabaseBaseFilename) { MOZ_ASSERT(!aFilename.IsEmpty()); MOZ_ASSERT(aDatabaseBaseFilename.IsEmpty()); NS_NAMED_LITERAL_STRING(sqlite, ".sqlite"); if (!StringEndsWith(aFilename, sqlite) || aFilename.Length() == sqlite.Length()) { return false; } MOZ_ASSERT(aFilename.Length() > sqlite.Length()); aDatabaseBaseFilename.Rebind(aFilename, 0, aFilename.Length() - sqlite.Length()); return true; } nsresult SerializeStructuredCloneFiles( PBackgroundParent* aBackgroundActor, Database* aDatabase, const nsTArray<StructuredCloneFile>& aFiles, bool aForPreprocess, FallibleTArray<SerializedStructuredCloneFile>& aResult) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aBackgroundActor); MOZ_ASSERT(aDatabase); MOZ_ASSERT(aResult.IsEmpty()); if (aFiles.IsEmpty()) { return NS_OK; } FileManager* fileManager = aDatabase->GetFileManager(); nsCOMPtr<nsIFile> directory = fileManager->GetCheckedDirectory(); if (NS_WARN_IF(!directory)) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } const uint32_t count = aFiles.Length(); if (NS_WARN_IF(!aResult.SetCapacity(count, fallible))) { return NS_ERROR_OUT_OF_MEMORY; } for (uint32_t index = 0; index < count; index++) { const StructuredCloneFile& file = aFiles[index]; if (aForPreprocess && file.mType != StructuredCloneFile::eWasmBytecode && file.mType != StructuredCloneFile::eWasmCompiled) { continue; } const int64_t fileId = file.mFileInfo->Id(); MOZ_ASSERT(fileId > 0); nsCOMPtr<nsIFile> nativeFile = fileManager->GetCheckedFileForId(directory, fileId); if (NS_WARN_IF(!nativeFile)) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } switch (file.mType) { case StructuredCloneFile::eBlob: { RefPtr<BlobImpl> impl = new BlobImplStoredFile(nativeFile, file.mFileInfo, /* aSnapshot */ false); PBlobParent* actor = BackgroundParent::GetOrCreateActorForBlobImpl(aBackgroundActor, impl); if (!actor) { // This can only fail if the child has crashed. IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } SerializedStructuredCloneFile* file = aResult.AppendElement(fallible); MOZ_ASSERT(file); file->file() = actor; file->type() = StructuredCloneFile::eBlob; break; } case StructuredCloneFile::eMutableFile: { if (aDatabase->IsFileHandleDisabled()) { SerializedStructuredCloneFile* file = aResult.AppendElement(fallible); MOZ_ASSERT(file); file->file() = null_t(); file->type() = StructuredCloneFile::eMutableFile; } else { RefPtr<MutableFile> actor = MutableFile::Create(nativeFile, aDatabase, file.mFileInfo); if (!actor) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } // Transfer ownership to IPDL. actor->SetActorAlive(); if (!aDatabase->SendPBackgroundMutableFileConstructor(actor, EmptyString(), EmptyString())) { // This can only fail if the child has crashed. IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } SerializedStructuredCloneFile* file = aResult.AppendElement(fallible); MOZ_ASSERT(file); file->file() = actor; file->type() = StructuredCloneFile::eMutableFile; } break; } case StructuredCloneFile::eStructuredClone: { SerializedStructuredCloneFile* file = aResult.AppendElement(fallible); MOZ_ASSERT(file); file->file() = null_t(); file->type() = StructuredCloneFile::eStructuredClone; break; } case StructuredCloneFile::eWasmBytecode: case StructuredCloneFile::eWasmCompiled: { if (!aForPreprocess) { SerializedStructuredCloneFile* serializedFile = aResult.AppendElement(fallible); MOZ_ASSERT(serializedFile); serializedFile->file() = null_t(); serializedFile->type() = file.mType; } else { RefPtr<BlobImpl> impl = new BlobImplStoredFile(nativeFile, file.mFileInfo, /* aSnapshot */ false); PBlobParent* actor = BackgroundParent::GetOrCreateActorForBlobImpl(aBackgroundActor, impl); if (!actor) { // This can only fail if the child has crashed. IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } SerializedStructuredCloneFile* serializedFile = aResult.AppendElement(fallible); MOZ_ASSERT(serializedFile); serializedFile->file() = actor; serializedFile->type() = file.mType; } break; } default: MOZ_CRASH("Should never get here!"); } } return NS_OK; } already_AddRefed<nsIFile> GetFileForFileInfo(FileInfo* aFileInfo) { FileManager* fileManager = aFileInfo->Manager(); nsCOMPtr<nsIFile> directory = fileManager->GetDirectory(); if (NS_WARN_IF(!directory)) { return nullptr; } nsCOMPtr<nsIFile> file = fileManager->GetFileForId(directory, aFileInfo->Id()); if (NS_WARN_IF(!file)) { return nullptr; } return file.forget(); } /******************************************************************************* * Globals ******************************************************************************/ // Counts the number of "live" Factory, FactoryOp and Database instances. uint64_t gBusyCount = 0; typedef nsTArray<RefPtr<FactoryOp>> FactoryOpArray; StaticAutoPtr<FactoryOpArray> gFactoryOps; // Maps a database id to information about live database actors. typedef nsClassHashtable<nsCStringHashKey, DatabaseActorInfo> DatabaseActorHashtable; StaticAutoPtr<DatabaseActorHashtable> gLiveDatabaseHashtable; StaticRefPtr<ConnectionPool> gConnectionPool; StaticRefPtr<FileHandleThreadPool> gFileHandleThreadPool; typedef nsDataHashtable<nsIDHashKey, DatabaseLoggingInfo*> DatabaseLoggingInfoHashtable; StaticAutoPtr<DatabaseLoggingInfoHashtable> gLoggingInfoHashtable; typedef nsDataHashtable<nsUint32HashKey, uint32_t> TelemetryIdHashtable; StaticAutoPtr<TelemetryIdHashtable> gTelemetryIdHashtable; // Protects all reads and writes to gTelemetryIdHashtable. StaticAutoPtr<Mutex> gTelemetryIdMutex; #ifdef DEBUG StaticRefPtr<DEBUGThreadSlower> 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<nsISupportsPriority> 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<nsIThreadInternal> 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<nsISupportsPriority> thread = do_QueryInterface(NS_GetCurrentThread()); MOZ_ASSERT(thread); MOZ_ALWAYS_SUCCEEDS( thread->SetPriority(nsISupportsPriority::PRIORITY_NORMAL)); } if (kDEBUGThreadSleepMS) { MOZ_ASSERT(gDEBUGThreadSlower); nsCOMPtr<nsIThreadInternal> thread = do_QueryInterface(NS_GetCurrentThread()); MOZ_ASSERT(thread); MOZ_ALWAYS_SUCCEEDS(thread->RemoveObserver(gDEBUGThreadSlower)); gDEBUGThreadSlower = nullptr; } #endif // DEBUG } } uint32_t TelemetryIdForFile(nsIFile* aFile) { // May be called on any thread! MOZ_ASSERT(aFile); MOZ_ASSERT(gTelemetryIdMutex); // The storage directory is structured like this: // // <profile>/storage/<persistence>/<origin>/idb/<filename>.sqlite // // For the purposes of this function we're only concerned with the // <persistence>, <origin>, and <filename> pieces. nsString filename; MOZ_ALWAYS_SUCCEEDS(aFile->GetLeafName(filename)); // Make sure we were given a database file. NS_NAMED_LITERAL_STRING(sqliteExtension, ".sqlite"); MOZ_ASSERT(StringEndsWith(filename, sqliteExtension)); filename.Truncate(filename.Length() - sqliteExtension.Length()); // Get the "idb" directory. nsCOMPtr<nsIFile> idbDirectory; MOZ_ALWAYS_SUCCEEDS(aFile->GetParent(getter_AddRefs(idbDirectory))); DebugOnly<nsString> idbLeafName; MOZ_ASSERT(NS_SUCCEEDED(idbDirectory->GetLeafName(idbLeafName))); MOZ_ASSERT(static_cast<nsString&>(idbLeafName).EqualsLiteral("idb")); // Get the <origin> directory. nsCOMPtr<nsIFile> 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 <persistence> directory. nsCOMPtr<nsIFile> persistenceDirectory; MOZ_ALWAYS_SUCCEEDS( originDirectory->GetParent(getter_AddRefs(persistenceDirectory))); nsString persistence; MOZ_ALWAYS_SUCCEEDS(persistenceDirectory->GetLeafName(persistence)); NS_NAMED_LITERAL_STRING(separator, "*"); uint32_t hashValue = HashString(persistence + separator + origin + separator + filename); MutexAutoLock lock(*gTelemetryIdMutex); if (!gTelemetryIdHashtable) { gTelemetryIdHashtable = new TelemetryIdHashtable(); } uint32_t id; if (!gTelemetryIdHashtable->Get(hashValue, &id)) { static uint32_t sNextId = 1; // We're locked, no need for atomics. id = sNextId++; gTelemetryIdHashtable->Put(hashValue, id); } return id; } } // namespace /******************************************************************************* * Exported functions ******************************************************************************/ PBackgroundIDBFactoryParent* AllocPBackgroundIDBFactoryParent(const LoggingInfo& aLoggingInfo) { AssertIsOnBackgroundThread(); if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread())) { return nullptr; } if (NS_WARN_IF(!aLoggingInfo.nextTransactionSerialNumber()) || NS_WARN_IF(!aLoggingInfo.nextVersionChangeTransactionSerialNumber()) || NS_WARN_IF(!aLoggingInfo.nextRequestSerialNumber())) { ASSERT_UNLESS_FUZZING(); return nullptr; } RefPtr<Factory> actor = Factory::Create(aLoggingInfo); MOZ_ASSERT(actor); return actor.forget().take(); } bool RecvPBackgroundIDBFactoryConstructor(PBackgroundIDBFactoryParent* aActor, const LoggingInfo& /* aLoggingInfo */) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); return true; } bool DeallocPBackgroundIDBFactoryParent(PBackgroundIDBFactoryParent* aActor) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); RefPtr<Factory> actor = dont_AddRef(static_cast<Factory*>(aActor)); return true; } PBackgroundIndexedDBUtilsParent* AllocPBackgroundIndexedDBUtilsParent() { AssertIsOnBackgroundThread(); RefPtr<Utils> actor = new Utils(); return actor.forget().take(); } bool DeallocPBackgroundIndexedDBUtilsParent(PBackgroundIndexedDBUtilsParent* aActor) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); RefPtr<Utils> actor = dont_AddRef(static_cast<Utils*>(aActor)); return true; } bool RecvFlushPendingFileDeletions() { AssertIsOnBackgroundThread(); RefPtr<FlushPendingFileDeletionsRunnable> runnable = new FlushPendingFileDeletionsRunnable(); MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(runnable.forget())); return true; } PIndexedDBPermissionRequestParent* AllocPIndexedDBPermissionRequestParent(Element* aOwnerElement, nsIPrincipal* aPrincipal) { MOZ_ASSERT(NS_IsMainThread()); RefPtr<PermissionRequestHelper> actor = new PermissionRequestHelper(aOwnerElement, aPrincipal); return actor.forget().take(); } bool RecvPIndexedDBPermissionRequestConstructor( PIndexedDBPermissionRequestParent* aActor) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aActor); auto* actor = static_cast<PermissionRequestHelper*>(aActor); PermissionRequestBase::PermissionValue permission; nsresult rv = actor->PromptIfNeeded(&permission); if (NS_FAILED(rv)) { return false; } if (permission != PermissionRequestBase::kPermissionPrompt) { Unused << PIndexedDBPermissionRequestParent::Send__delete__(actor, permission); } return true; } bool DeallocPIndexedDBPermissionRequestParent( PIndexedDBPermissionRequestParent* aActor) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aActor); RefPtr<PermissionRequestHelper> actor = dont_AddRef(static_cast<PermissionRequestHelper*>(aActor)); return true; } already_AddRefed<mozilla::dom::quota::Client> CreateQuotaClient() { AssertIsOnBackgroundThread(); RefPtr<QuotaClient> client = new QuotaClient(); return client.forget(); } FileHandleThreadPool* GetFileHandleThreadPool() { AssertIsOnBackgroundThread(); if (!gFileHandleThreadPool) { RefPtr<FileHandleThreadPool> fileHandleThreadPool = FileHandleThreadPool::Create(); if (NS_WARN_IF(!fileHandleThreadPool)) { return nullptr; } gFileHandleThreadPool = fileHandleThreadPool; } return gFileHandleThreadPool; } /******************************************************************************* * DatabaseConnection implementation ******************************************************************************/ DatabaseConnection::DatabaseConnection( mozIStorageConnection* aStorageConnection, FileManager* aFileManager) : mStorageConnection(aStorageConnection) , mFileManager(aFileManager) , mInReadTransaction(false) , mInWriteTransaction(false) #ifdef DEBUG , mDEBUGSavepointCount(0) , mDEBUGThread(PR_GetCurrentThread()) #endif { AssertIsOnConnectionThread(); MOZ_ASSERT(aStorageConnection); MOZ_ASSERT(aFileManager); } DatabaseConnection::~DatabaseConnection() { MOZ_ASSERT(!mStorageConnection); MOZ_ASSERT(!mFileManager); MOZ_ASSERT(!mCachedStatements.Count()); MOZ_ASSERT(!mUpdateRefcountFunction); MOZ_ASSERT(!mInWriteTransaction); MOZ_ASSERT(!mDEBUGSavepointCount); } nsresult DatabaseConnection::Init() { AssertIsOnConnectionThread(); MOZ_ASSERT(!mInReadTransaction); MOZ_ASSERT(!mInWriteTransaction); CachedStatement stmt; nsresult rv = GetCachedStatement(NS_LITERAL_CSTRING("BEGIN;"), &stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->Execute(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } mInReadTransaction = true; return NS_OK; } nsresult DatabaseConnection::GetCachedStatement(const nsACString& aQuery, CachedStatement* aCachedStatement) { AssertIsOnConnectionThread(); MOZ_ASSERT(!aQuery.IsEmpty()); MOZ_ASSERT(aCachedStatement); MOZ_ASSERT(mStorageConnection); PROFILER_LABEL("IndexedDB", "DatabaseConnection::GetCachedStatement", js::ProfileEntry::Category::STORAGE); nsCOMPtr<mozIStorageStatement> stmt; if (!mCachedStatements.Get(aQuery, getter_AddRefs(stmt))) { nsresult rv = mStorageConnection->CreateStatement(aQuery, getter_AddRefs(stmt)); if (NS_FAILED(rv)) { #ifdef DEBUG nsCString msg; MOZ_ALWAYS_SUCCEEDS(mStorageConnection->GetLastErrorString(msg)); nsAutoCString error = NS_LITERAL_CSTRING("The statement '") + aQuery + NS_LITERAL_CSTRING("' failed to compile with the error message '") + msg + NS_LITERAL_CSTRING("'."); NS_WARNING(error.get()); #endif return rv; } mCachedStatements.Put(aQuery, stmt); } aCachedStatement->Assign(this, stmt.forget()); return NS_OK; } nsresult DatabaseConnection::BeginWriteTransaction() { AssertIsOnConnectionThread(); MOZ_ASSERT(mStorageConnection); MOZ_ASSERT(mInReadTransaction); MOZ_ASSERT(!mInWriteTransaction); PROFILER_LABEL("IndexedDB", "DatabaseConnection::BeginWriteTransaction", js::ProfileEntry::Category::STORAGE); // Release our read locks. CachedStatement rollbackStmt; nsresult rv = GetCachedStatement(NS_LITERAL_CSTRING("ROLLBACK;"), &rollbackStmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = rollbackStmt->Execute(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } mInReadTransaction = false; if (!mUpdateRefcountFunction) { MOZ_ASSERT(mFileManager); RefPtr<UpdateRefcountFunction> function = new UpdateRefcountFunction(this, mFileManager); rv = mStorageConnection->CreateFunction(NS_LITERAL_CSTRING("update_refcount"), /* aNumArguments */ 2, function); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } mUpdateRefcountFunction.swap(function); } CachedStatement beginStmt; rv = GetCachedStatement(NS_LITERAL_CSTRING("BEGIN IMMEDIATE;"), &beginStmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = beginStmt->Execute(); if (rv == NS_ERROR_STORAGE_BUSY) { 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. 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; } } } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } mInWriteTransaction = true; return NS_OK; } nsresult DatabaseConnection::CommitWriteTransaction() { AssertIsOnConnectionThread(); MOZ_ASSERT(mStorageConnection); MOZ_ASSERT(!mInReadTransaction); MOZ_ASSERT(mInWriteTransaction); PROFILER_LABEL("IndexedDB", "DatabaseConnection::CommitWriteTransaction", js::ProfileEntry::Category::STORAGE); CachedStatement stmt; nsresult rv = GetCachedStatement(NS_LITERAL_CSTRING("COMMIT;"), &stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->Execute(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } mInWriteTransaction = false; return NS_OK; } void DatabaseConnection::RollbackWriteTransaction() { AssertIsOnConnectionThread(); MOZ_ASSERT(!mInReadTransaction); MOZ_ASSERT(mStorageConnection); PROFILER_LABEL("IndexedDB", "DatabaseConnection::RollbackWriteTransaction", js::ProfileEntry::Category::STORAGE); if (!mInWriteTransaction) { return; } DatabaseConnection::CachedStatement stmt; nsresult rv = GetCachedStatement(NS_LITERAL_CSTRING("ROLLBACK;"), &stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return; } // This may fail if SQLite already rolled back the transaction so ignore any // errors. Unused << stmt->Execute(); mInWriteTransaction = false; } void DatabaseConnection::FinishWriteTransaction() { AssertIsOnConnectionThread(); MOZ_ASSERT(mStorageConnection); MOZ_ASSERT(!mInReadTransaction); MOZ_ASSERT(!mInWriteTransaction); PROFILER_LABEL("IndexedDB", "DatabaseConnection::FinishWriteTransaction", js::ProfileEntry::Category::STORAGE); if (mUpdateRefcountFunction) { mUpdateRefcountFunction->Reset(); } CachedStatement stmt; nsresult rv = GetCachedStatement(NS_LITERAL_CSTRING("BEGIN;"), &stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return; } rv = stmt->Execute(); if (NS_WARN_IF(NS_FAILED(rv))) { return; } mInReadTransaction = true; } nsresult DatabaseConnection::StartSavepoint() { AssertIsOnConnectionThread(); MOZ_ASSERT(mStorageConnection); MOZ_ASSERT(mUpdateRefcountFunction); MOZ_ASSERT(mInWriteTransaction); PROFILER_LABEL("IndexedDB", "DatabaseConnection::StartSavepoint", js::ProfileEntry::Category::STORAGE); CachedStatement stmt; nsresult rv = GetCachedStatement(NS_LITERAL_CSTRING(SAVEPOINT_CLAUSE), &stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->Execute(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } mUpdateRefcountFunction->StartSavepoint(); #ifdef DEBUG MOZ_ASSERT(mDEBUGSavepointCount < UINT32_MAX); mDEBUGSavepointCount++; #endif return NS_OK; } nsresult DatabaseConnection::ReleaseSavepoint() { AssertIsOnConnectionThread(); MOZ_ASSERT(mStorageConnection); MOZ_ASSERT(mUpdateRefcountFunction); MOZ_ASSERT(mInWriteTransaction); PROFILER_LABEL("IndexedDB", "DatabaseConnection::ReleaseSavepoint", js::ProfileEntry::Category::STORAGE); CachedStatement stmt; nsresult rv = GetCachedStatement( NS_LITERAL_CSTRING("RELEASE " SAVEPOINT_CLAUSE), &stmt); if (NS_SUCCEEDED(rv)) { rv = stmt->Execute(); if (NS_SUCCEEDED(rv)) { mUpdateRefcountFunction->ReleaseSavepoint(); #ifdef DEBUG MOZ_ASSERT(mDEBUGSavepointCount); mDEBUGSavepointCount--; #endif } } return rv; } nsresult DatabaseConnection::RollbackSavepoint() { AssertIsOnConnectionThread(); MOZ_ASSERT(mStorageConnection); MOZ_ASSERT(mUpdateRefcountFunction); MOZ_ASSERT(mInWriteTransaction); PROFILER_LABEL("IndexedDB", "DatabaseConnection::RollbackSavepoint", js::ProfileEntry::Category::STORAGE); #ifdef DEBUG MOZ_ASSERT(mDEBUGSavepointCount); mDEBUGSavepointCount--; #endif mUpdateRefcountFunction->RollbackSavepoint(); CachedStatement stmt; nsresult rv = GetCachedStatement( NS_LITERAL_CSTRING("ROLLBACK TO " SAVEPOINT_CLAUSE), &stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // 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); PROFILER_LABEL("IndexedDB", "DatabaseConnection::CheckpointInternal", js::ProfileEntry::Category::STORAGE); 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(");"); CachedStatement stmt; nsresult rv = GetCachedStatement(stmtString, &stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->Execute(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } void DatabaseConnection::DoIdleProcessing(bool aNeedsCheckpoint) { AssertIsOnConnectionThread(); MOZ_ASSERT(mInReadTransaction); MOZ_ASSERT(!mInWriteTransaction); PROFILER_LABEL("IndexedDB", "DatabaseConnection::DoIdleProcessing", js::ProfileEntry::Category::STORAGE); DatabaseConnection::CachedStatement freelistStmt; uint32_t freelistCount; nsresult rv = GetFreelistCount(freelistStmt, &freelistCount); if (NS_WARN_IF(NS_FAILED(rv))) { freelistCount = 0; } CachedStatement rollbackStmt; CachedStatement beginStmt; if (aNeedsCheckpoint || freelistCount) { rv = GetCachedStatement(NS_LITERAL_CSTRING("ROLLBACK;"), &rollbackStmt); if (NS_WARN_IF(NS_FAILED(rv))) { return; } rv = GetCachedStatement(NS_LITERAL_CSTRING("BEGIN;"), &beginStmt); if (NS_WARN_IF(NS_FAILED(rv))) { return; } // Release the connection's normal transaction. It's possible that it could // fail, but that isn't a problem here. Unused << rollbackStmt->Execute(); mInReadTransaction = false; } bool freedSomePages = false; if (freelistCount) { rv = ReclaimFreePagesWhileIdle(freelistStmt, rollbackStmt, freelistCount, aNeedsCheckpoint, &freedSomePages); if (NS_WARN_IF(NS_FAILED(rv))) { MOZ_ASSERT(!freedSomePages); } // Make sure we didn't leave a transaction running. MOZ_ASSERT(!mInReadTransaction); MOZ_ASSERT(!mInWriteTransaction); } // Truncate the WAL if we were asked to or if we managed to free some space. if (aNeedsCheckpoint || freedSomePages) { rv = CheckpointInternal(CheckpointMode::Truncate); Unused << NS_WARN_IF(NS_FAILED(rv)); } // Finally try to restart the read transaction if we rolled it back earlier. if (beginStmt) { rv = beginStmt->Execute(); if (NS_SUCCEEDED(rv)) { mInReadTransaction = true; } else { NS_WARNING("Falied to restart read transaction!"); } } } nsresult DatabaseConnection::ReclaimFreePagesWhileIdle( CachedStatement& aFreelistStatement, CachedStatement& aRollbackStatement, uint32_t aFreelistCount, bool aNeedsCheckpoint, bool* aFreedSomePages) { AssertIsOnConnectionThread(); MOZ_ASSERT(aFreelistStatement); MOZ_ASSERT(aRollbackStatement); MOZ_ASSERT(aFreelistCount); MOZ_ASSERT(aFreedSomePages); MOZ_ASSERT(!mInReadTransaction); MOZ_ASSERT(!mInWriteTransaction); PROFILER_LABEL("IndexedDB", "DatabaseConnection::ReclaimFreePagesWhileIdle", js::ProfileEntry::Category::STORAGE); // Make sure we don't keep working if anything else needs this thread. nsIThread* currentThread = NS_GetCurrentThread(); MOZ_ASSERT(currentThread); if (NS_HasPendingEvents(currentThread)) { *aFreedSomePages = false; return NS_OK; } // 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. nsAutoCString stmtString; stmtString.AssignLiteral("PRAGMA incremental_vacuum("); stmtString.AppendInt(std::max(uint64_t(1), uint64_t(aFreelistCount / 10))); stmtString.AppendLiteral(");"); // Make all the statements we'll need up front. CachedStatement incrementalVacuumStmt; nsresult rv = GetCachedStatement(stmtString, &incrementalVacuumStmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } CachedStatement beginImmediateStmt; rv = GetCachedStatement(NS_LITERAL_CSTRING("BEGIN IMMEDIATE;"), &beginImmediateStmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } CachedStatement commitStmt; rv = GetCachedStatement(NS_LITERAL_CSTRING("COMMIT;"), &commitStmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } 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. rv = CheckpointInternal(CheckpointMode::Restart); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } // Start the write transaction. rv = beginImmediateStmt->Execute(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } mInWriteTransaction = true; bool freedSomePages = false; while (aFreelistCount) { if (NS_HasPendingEvents(currentThread)) { // Something else wants to use the thread so roll back this transaction. // It's ok if we never make progress here because the idle service should // eventually reclaim this space. rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; break; } rv = incrementalVacuumStmt->Execute(); if (NS_WARN_IF(NS_FAILED(rv))) { break; } freedSomePages = true; rv = GetFreelistCount(aFreelistStatement, &aFreelistCount); if (NS_WARN_IF(NS_FAILED(rv))) { break; } } if (NS_SUCCEEDED(rv) && freedSomePages) { // Commit the write transaction. rv = commitStmt->Execute(); if (NS_SUCCEEDED(rv)) { mInWriteTransaction = false; } else { NS_WARNING("Failed to commit!"); } } if (NS_FAILED(rv)) { MOZ_ASSERT(mInWriteTransaction); // Something failed, make sure we roll everything back. Unused << aRollbackStatement->Execute(); mInWriteTransaction = false; return rv; } *aFreedSomePages = freedSomePages; return NS_OK; } nsresult DatabaseConnection::GetFreelistCount(CachedStatement& aCachedStatement, uint32_t* aFreelistCount) { AssertIsOnConnectionThread(); MOZ_ASSERT(aFreelistCount); PROFILER_LABEL("IndexedDB", "DatabaseConnection::GetFreelistCount", js::ProfileEntry::Category::STORAGE); nsresult rv; if (!aCachedStatement) { rv = GetCachedStatement(NS_LITERAL_CSTRING("PRAGMA freelist_count;"), &aCachedStatement); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } bool hasResult; rv = aCachedStatement->ExecuteStep(&hasResult); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(hasResult); // Make sure this statement is reset when leaving this function since we're // not using the normal stack-based protection of CachedStatement. mozStorageStatementScoper scoper(aCachedStatement); int32_t freelistCount; rv = aCachedStatement->GetInt32(0, &freelistCount); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(freelistCount >= 0); *aFreelistCount = uint32_t(freelistCount); return NS_OK; } void DatabaseConnection::Close() { AssertIsOnConnectionThread(); MOZ_ASSERT(mStorageConnection); MOZ_ASSERT(!mDEBUGSavepointCount); MOZ_ASSERT(!mInWriteTransaction); PROFILER_LABEL("IndexedDB", "DatabaseConnection::Close", js::ProfileEntry::Category::STORAGE); if (mUpdateRefcountFunction) { MOZ_ALWAYS_SUCCEEDS( mStorageConnection->RemoveFunction( NS_LITERAL_CSTRING("update_refcount"))); mUpdateRefcountFunction = nullptr; } mCachedStatements.Clear(); MOZ_ALWAYS_SUCCEEDS(mStorageConnection->Close()); mStorageConnection = nullptr; mFileManager = nullptr; } nsresult DatabaseConnection::DisableQuotaChecks() { AssertIsOnConnectionThread(); MOZ_ASSERT(mStorageConnection); if (!mQuotaObject) { MOZ_ASSERT(!mJournalQuotaObject); nsresult rv = mStorageConnection->GetQuotaObjects( getter_AddRefs(mQuotaObject), getter_AddRefs(mJournalQuotaObject)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(mQuotaObject); MOZ_ASSERT(mJournalQuotaObject); } mQuotaObject->DisableQuotaCheck(); mJournalQuotaObject->DisableQuotaCheck(); return NS_OK; } void DatabaseConnection::EnableQuotaChecks() { AssertIsOnConnectionThread(); MOZ_ASSERT(mQuotaObject); MOZ_ASSERT(mJournalQuotaObject); RefPtr<QuotaObject> quotaObject; RefPtr<QuotaObject> journalQuotaObject; mQuotaObject.swap(quotaObject); mJournalQuotaObject.swap(journalQuotaObject); quotaObject->EnableQuotaCheck(); journalQuotaObject->EnableQuotaCheck(); int64_t fileSize; nsresult rv = GetFileSize(quotaObject->Path(), &fileSize); if (NS_WARN_IF(NS_FAILED(rv))) { return; } int64_t journalFileSize; rv = GetFileSize(journalQuotaObject->Path(), &journalFileSize); if (NS_WARN_IF(NS_FAILED(rv))) { return; } DebugOnly<bool> result = journalQuotaObject->MaybeUpdateSize(journalFileSize, /* aTruncate */ true); MOZ_ASSERT(result); result = quotaObject->MaybeUpdateSize(fileSize, /* aTruncate */ true); MOZ_ASSERT(result); } nsresult DatabaseConnection::GetFileSize(const nsAString& aPath, int64_t* aResult) { MOZ_ASSERT(!aPath.IsEmpty()); MOZ_ASSERT(aResult); nsresult rv; nsCOMPtr<nsIFile> file = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = file->InitWithPath(aPath); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } int64_t fileSize; bool exists; rv = file->Exists(&exists); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (exists) { rv = file->GetFileSize(&fileSize); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } else { fileSize = 0; } *aResult = fileSize; return NS_OK; } DatabaseConnection:: CachedStatement::CachedStatement() #ifdef DEBUG : mDEBUGConnection(nullptr) #endif { AssertIsOnConnectionThread(); MOZ_COUNT_CTOR(DatabaseConnection::CachedStatement); } DatabaseConnection:: CachedStatement::~CachedStatement() { AssertIsOnConnectionThread(); MOZ_COUNT_DTOR(DatabaseConnection::CachedStatement); } DatabaseConnection:: CachedStatement::operator mozIStorageStatement*() const { AssertIsOnConnectionThread(); return mStatement; } mozIStorageStatement* DatabaseConnection:: CachedStatement::operator->() const { AssertIsOnConnectionThread(); MOZ_ASSERT(mStatement); return mStatement; } void DatabaseConnection:: CachedStatement::Reset() { AssertIsOnConnectionThread(); MOZ_ASSERT_IF(mStatement, mScoper); if (mStatement) { mScoper.reset(); mScoper.emplace(mStatement); } } void DatabaseConnection:: CachedStatement::Assign(DatabaseConnection* aConnection, already_AddRefed<mozIStorageStatement> aStatement) { #ifdef DEBUG MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); MOZ_ASSERT_IF(mDEBUGConnection, mDEBUGConnection == aConnection); mDEBUGConnection = aConnection; #endif AssertIsOnConnectionThread(); mScoper.reset(); mStatement = aStatement; if (mStatement) { mScoper.emplace(mStatement); } } 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::READ_WRITE || mDEBUGTransaction->GetMode() == IDBTransaction::READ_WRITE_FLUSH || mDEBUGTransaction->GetMode() == IDBTransaction::CLEANUP || mDEBUGTransaction->GetMode() == IDBTransaction::VERSION_CHANGE); if (NS_FAILED(mConnection->RollbackSavepoint())) { NS_WARNING("Failed to rollback savepoint!"); } } } nsresult DatabaseConnection:: AutoSavepoint::Start(const TransactionBase* aTransaction) { MOZ_ASSERT(aTransaction); MOZ_ASSERT(aTransaction->GetMode() == IDBTransaction::READ_WRITE || aTransaction->GetMode() == IDBTransaction::READ_WRITE_FLUSH || aTransaction->GetMode() == IDBTransaction::CLEANUP || aTransaction->GetMode() == IDBTransaction::VERSION_CHANGE); DatabaseConnection* connection = aTransaction->GetDatabase()->GetConnection(); MOZ_ASSERT(connection); connection->AssertIsOnConnectionThread(); MOZ_ASSERT(!mConnection); MOZ_ASSERT(!mDEBUGTransaction); nsresult rv = connection->StartSavepoint(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } mConnection = connection; #ifdef DEBUG mDEBUGTransaction = aTransaction; #endif return NS_OK; } nsresult DatabaseConnection:: AutoSavepoint::Commit() { MOZ_ASSERT(mConnection); mConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(mDEBUGTransaction); nsresult rv = mConnection->ReleaseSavepoint(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } mConnection = nullptr; #ifdef DEBUG mDEBUGTransaction = nullptr; #endif return NS_OK; } DatabaseConnection:: UpdateRefcountFunction::UpdateRefcountFunction(DatabaseConnection* aConnection, FileManager* aFileManager) : mConnection(aConnection) , mFileManager(aFileManager) , mInSavepoint(false) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(aFileManager); } nsresult DatabaseConnection:: UpdateRefcountFunction::WillCommit() { MOZ_ASSERT(mConnection); mConnection->AssertIsOnConnectionThread(); PROFILER_LABEL("IndexedDB", "DatabaseConnection::UpdateRefcountFunction::WillCommit", js::ProfileEntry::Category::STORAGE); DatabaseUpdateFunction function(this); for (auto iter = mFileInfoEntries.ConstIter(); !iter.Done(); iter.Next()) { auto key = iter.Key(); FileInfoEntry* value = iter.Data(); MOZ_ASSERT(value); if (value->mDelta && !function.Update(key, value->mDelta)) { break; } } nsresult rv = function.ErrorCode(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = CreateJournals(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } void DatabaseConnection:: UpdateRefcountFunction::DidCommit() { MOZ_ASSERT(mConnection); mConnection->AssertIsOnConnectionThread(); PROFILER_LABEL("IndexedDB", "DatabaseConnection::UpdateRefcountFunction::DidCommit", js::ProfileEntry::Category::STORAGE); for (auto iter = mFileInfoEntries.ConstIter(); !iter.Done(); iter.Next()) { FileInfoEntry* value = iter.Data(); MOZ_ASSERT(value); if (value->mDelta) { value->mFileInfo->UpdateDBRefs(value->mDelta); } } if (NS_FAILED(RemoveJournals(mJournalsToRemoveAfterCommit))) { NS_WARNING("RemoveJournals failed!"); } } void DatabaseConnection:: UpdateRefcountFunction::DidAbort() { MOZ_ASSERT(mConnection); mConnection->AssertIsOnConnectionThread(); PROFILER_LABEL("IndexedDB", "DatabaseConnection::UpdateRefcountFunction::DidAbort", js::ProfileEntry::Category::STORAGE); if (NS_FAILED(RemoveJournals(mJournalsToRemoveAfterAbort))) { NS_WARNING("RemoveJournals failed!"); } } 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 (auto iter = mSavepointEntriesIndex.ConstIter(); !iter.Done(); iter.Next()) { auto value = iter.Data(); value->mDelta -= value->mSavepointDelta; } mInSavepoint = false; mSavepointEntriesIndex.Clear(); } void DatabaseConnection:: UpdateRefcountFunction::Reset() { MOZ_ASSERT(mConnection); mConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(!mSavepointEntriesIndex.Count()); MOZ_ASSERT(!mInSavepoint); class MOZ_STACK_CLASS CustomCleanupCallback final : public FileInfo::CustomCleanupCallback { nsCOMPtr<nsIFile> mDirectory; nsCOMPtr<nsIFile> mJournalDirectory; public: virtual nsresult Cleanup(FileManager* aFileManager, int64_t aId) { if (!mDirectory) { MOZ_ASSERT(!mJournalDirectory); mDirectory = aFileManager->GetDirectory(); if (NS_WARN_IF(!mDirectory)) { return NS_ERROR_FAILURE; } mJournalDirectory = aFileManager->GetJournalDirectory(); if (NS_WARN_IF(!mJournalDirectory)) { return NS_ERROR_FAILURE; } } nsCOMPtr<nsIFile> file = aFileManager->GetFileForId(mDirectory, aId); if (NS_WARN_IF(!file)) { return NS_ERROR_FAILURE; } nsresult rv; int64_t fileSize; if (aFileManager->EnforcingQuota()) { rv = file->GetFileSize(&fileSize); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } rv = file->Remove(false); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (aFileManager->EnforcingQuota()) { QuotaManager* quotaManager = QuotaManager::Get(); MOZ_ASSERT(quotaManager); quotaManager->DecreaseUsageForOrigin(aFileManager->Type(), aFileManager->Group(), aFileManager->Origin(), fileSize); } file = aFileManager->GetFileForId(mJournalDirectory, aId); if (NS_WARN_IF(!file)) { return NS_ERROR_FAILURE; } rv = file->Remove(false); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } }; mJournalsToCreateBeforeCommit.Clear(); mJournalsToRemoveAfterCommit.Clear(); mJournalsToRemoveAfterAbort.Clear(); // FileInfo 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 (auto iter = mFileInfoEntries.ConstIter(); !iter.Done(); iter.Next()) { FileInfoEntry* value = iter.Data(); MOZ_ASSERT(value); FileInfo* fileInfo = value->mFileInfo.forget().take(); MOZ_ASSERT(fileInfo); CustomCleanupCallback customCleanupCallback; fileInfo->Release(&customCleanupCallback); } mFileInfoEntries.Clear(); } nsresult DatabaseConnection:: UpdateRefcountFunction::ProcessValue(mozIStorageValueArray* aValues, int32_t aIndex, UpdateType aUpdateType) { MOZ_ASSERT(mConnection); mConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(aValues); PROFILER_LABEL("IndexedDB", "DatabaseConnection::UpdateRefcountFunction::ProcessValue", js::ProfileEntry::Category::STORAGE); int32_t type; nsresult rv = aValues->GetTypeOfIndex(aIndex, &type); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (type == mozIStorageValueArray::VALUE_TYPE_NULL) { return NS_OK; } nsString ids; rv = aValues->GetString(aIndex, ids); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsTArray<StructuredCloneFile> files; rv = DeserializeStructuredCloneFiles(mFileManager, ids, files, nullptr); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } for (uint32_t i = 0; i < files.Length(); i++) { const StructuredCloneFile& file = files[i]; const int64_t id = file.mFileInfo->Id(); MOZ_ASSERT(id > 0); FileInfoEntry* entry; if (!mFileInfoEntries.Get(id, &entry)) { entry = new FileInfoEntry(file.mFileInfo); mFileInfoEntries.Put(id, entry); } if (mInSavepoint) { mSavepointEntriesIndex.Put(id, entry); } switch (aUpdateType) { case UpdateType::Increment: entry->mDelta++; if (mInSavepoint) { entry->mSavepointDelta++; } break; case UpdateType::Decrement: entry->mDelta--; if (mInSavepoint) { entry->mSavepointDelta--; } break; default: MOZ_CRASH("Unknown update type!"); } } return NS_OK; } nsresult DatabaseConnection:: UpdateRefcountFunction::CreateJournals() { MOZ_ASSERT(mConnection); mConnection->AssertIsOnConnectionThread(); PROFILER_LABEL("IndexedDB", "DatabaseConnection::UpdateRefcountFunction::CreateJournals", js::ProfileEntry::Category::STORAGE); nsCOMPtr<nsIFile> journalDirectory = mFileManager->GetJournalDirectory(); if (NS_WARN_IF(!journalDirectory)) { return NS_ERROR_FAILURE; } for (uint32_t i = 0; i < mJournalsToCreateBeforeCommit.Length(); i++) { int64_t id = mJournalsToCreateBeforeCommit[i]; nsCOMPtr<nsIFile> file = mFileManager->GetFileForId(journalDirectory, id); if (NS_WARN_IF(!file)) { return NS_ERROR_FAILURE; } nsresult rv = file->Create(nsIFile::NORMAL_FILE_TYPE, 0644); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } mJournalsToRemoveAfterAbort.AppendElement(id); } return NS_OK; } nsresult DatabaseConnection:: UpdateRefcountFunction::RemoveJournals(const nsTArray<int64_t>& aJournals) { MOZ_ASSERT(mConnection); mConnection->AssertIsOnConnectionThread(); PROFILER_LABEL("IndexedDB", "DatabaseConnection::UpdateRefcountFunction::RemoveJournals", js::ProfileEntry::Category::STORAGE); nsCOMPtr<nsIFile> journalDirectory = mFileManager->GetJournalDirectory(); if (NS_WARN_IF(!journalDirectory)) { return NS_ERROR_FAILURE; } for (uint32_t index = 0; index < aJournals.Length(); index++) { nsCOMPtr<nsIFile> file = mFileManager->GetFileForId(journalDirectory, aJournals[index]); if (NS_WARN_IF(!file)) { return NS_ERROR_FAILURE; } if (NS_FAILED(file->Remove(false))) { NS_WARNING("Failed to removed journal!"); } } return NS_OK; } NS_IMPL_ISUPPORTS(DatabaseConnection::UpdateRefcountFunction, mozIStorageFunction) NS_IMETHODIMP DatabaseConnection:: UpdateRefcountFunction::OnFunctionCall(mozIStorageValueArray* aValues, nsIVariant** _retval) { MOZ_ASSERT(aValues); MOZ_ASSERT(_retval); PROFILER_LABEL("IndexedDB", "DatabaseConnection::UpdateRefcountFunction::OnFunctionCall", js::ProfileEntry::Category::STORAGE); uint32_t numEntries; nsresult rv = aValues->GetNumEntries(&numEntries); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(numEntries == 2); #ifdef DEBUG { int32_t type1 = mozIStorageValueArray::VALUE_TYPE_NULL; MOZ_ASSERT(NS_SUCCEEDED(aValues->GetTypeOfIndex(0, &type1))); int32_t type2 = mozIStorageValueArray::VALUE_TYPE_NULL; MOZ_ASSERT(NS_SUCCEEDED(aValues->GetTypeOfIndex(1, &type2))); MOZ_ASSERT(!(type1 == mozIStorageValueArray::VALUE_TYPE_NULL && type2 == mozIStorageValueArray::VALUE_TYPE_NULL)); } #endif rv = ProcessValue(aValues, 0, UpdateType::Decrement); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = ProcessValue(aValues, 1, UpdateType::Increment); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } bool DatabaseConnection::UpdateRefcountFunction:: DatabaseUpdateFunction::Update(int64_t aId, int32_t aDelta) { nsresult rv = UpdateInternal(aId, aDelta); if (NS_FAILED(rv)) { mErrorCode = rv; return false; } return true; } nsresult DatabaseConnection::UpdateRefcountFunction:: DatabaseUpdateFunction::UpdateInternal(int64_t aId, int32_t aDelta) { MOZ_ASSERT(mFunction); PROFILER_LABEL("IndexedDB", "DatabaseConnection::UpdateRefcountFunction::" "DatabaseUpdateFunction::UpdateInternal", js::ProfileEntry::Category::STORAGE); DatabaseConnection* connection = mFunction->mConnection; MOZ_ASSERT(connection); connection->AssertIsOnConnectionThread(); MOZ_ASSERT(connection->GetStorageConnection()); nsresult rv; if (!mUpdateStatement) { rv = connection->GetCachedStatement(NS_LITERAL_CSTRING( "UPDATE file " "SET refcount = refcount + :delta " "WHERE id = :id"), &mUpdateStatement); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } mozStorageStatementScoper updateScoper(mUpdateStatement); rv = mUpdateStatement->BindInt32ByName(NS_LITERAL_CSTRING("delta"), aDelta); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = mUpdateStatement->BindInt64ByName(NS_LITERAL_CSTRING("id"), aId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = mUpdateStatement->Execute(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } int32_t rows; rv = connection->GetStorageConnection()->GetAffectedRows(&rows); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (rows > 0) { if (!mSelectStatement) { rv = connection->GetCachedStatement(NS_LITERAL_CSTRING( "SELECT id " "FROM file " "WHERE id = :id"), &mSelectStatement); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } mozStorageStatementScoper selectScoper(mSelectStatement); rv = mSelectStatement->BindInt64ByName(NS_LITERAL_CSTRING("id"), aId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } bool hasResult; rv = mSelectStatement->ExecuteStep(&hasResult); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (!hasResult) { // Don't have to create the journal here, we can create all at once, // just before commit mFunction->mJournalsToCreateBeforeCommit.AppendElement(aId); } return NS_OK; } if (!mInsertStatement) { rv = connection->GetCachedStatement(NS_LITERAL_CSTRING( "INSERT INTO file (id, refcount) " "VALUES(:id, :delta)"), &mInsertStatement); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } mozStorageStatementScoper insertScoper(mInsertStatement); rv = mInsertStatement->BindInt64ByName(NS_LITERAL_CSTRING("id"), aId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = mInsertStatement->BindInt32ByName(NS_LITERAL_CSTRING("delta"), aDelta); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = mInsertStatement->Execute(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } mFunction->mJournalsToRemoveAfterCommit.AppendElement(aId); return NS_OK; } /******************************************************************************* * ConnectionPool implementation ******************************************************************************/ ConnectionPool::ConnectionPool() : mDatabasesMutex("ConnectionPool::mDatabasesMutex") , mIdleTimer(do_CreateInstance(NS_TIMER_CONTRACTID)) , mNextTransactionId(0) , mTotalThreadCount(0) , mShutdownRequested(false) , mShutdownComplete(false) #ifdef DEBUG , mDEBUGOwningThread(PR_GetCurrentThread()) #endif { 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); } #ifdef DEBUG void ConnectionPool::AssertIsOnOwningThread() const { MOZ_ASSERT(mDEBUGOwningThread); MOZ_ASSERT(PR_GetCurrentThread() == mDEBUGOwningThread); } #endif // DEBUG // static void ConnectionPool::IdleTimerCallback(nsITimer* aTimer, void* aClosure) { MOZ_ASSERT(aTimer); PROFILER_LABEL("IndexedDB", "ConnectionPool::IdleTimerCallback", js::ProfileEntry::Category::STORAGE); auto* self = static_cast<ConnectionPool*>(aClosure); MOZ_ASSERT(self); 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. TimeStamp now = TimeStamp::NowLoRes() + TimeDuration::FromMilliseconds(500); uint32_t index = 0; for (uint32_t count = self->mIdleDatabases.Length(); index < count; index++) { IdleDatabaseInfo& info = self->mIdleDatabases[index]; if (now >= info.mIdleTime) { if (info.mDatabaseInfo->mIdle) { self->PerformIdleDatabaseMaintenance(info.mDatabaseInfo); } else { self->CloseDatabase(info.mDatabaseInfo); } } else { break; } } if (index) { self->mIdleDatabases.RemoveElementsAt(0, index); index = 0; } for (uint32_t count = self->mIdleThreads.Length(); index < count; index++) { IdleThreadInfo& info = self->mIdleThreads[index]; MOZ_ASSERT(info.mThreadInfo.mThread); MOZ_ASSERT(info.mThreadInfo.mRunnable); if (now >= info.mIdleTime) { self->ShutdownThread(info.mThreadInfo); } else { break; } } if (index) { self->mIdleThreads.RemoveElementsAt(0, index); } self->AdjustIdleTimer(); } nsresult ConnectionPool::GetOrCreateConnection(const Database* aDatabase, DatabaseConnection** aConnection) { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(aDatabase); PROFILER_LABEL("IndexedDB", "ConnectionPool::GetOrCreateConnection", js::ProfileEntry::Category::STORAGE); DatabaseInfo* dbInfo; { MutexAutoLock lock(mDatabasesMutex); dbInfo = mDatabases.Get(aDatabase->Id()); } MOZ_ASSERT(dbInfo); RefPtr<DatabaseConnection> connection = dbInfo->mConnection; if (!connection) { MOZ_ASSERT(!dbInfo->mDEBUGConnectionThread); nsCOMPtr<mozIStorageConnection> storageConnection; nsresult rv = GetStorageConnection(aDatabase->FilePath(), aDatabase->Type(), aDatabase->Group(), aDatabase->Origin(), aDatabase->TelemetryId(), getter_AddRefs(storageConnection)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } connection = new DatabaseConnection(storageConnection, aDatabase->GetFileManager()); rv = connection->Init(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } dbInfo->mConnection = connection; IDB_DEBUG_LOG(("ConnectionPool created connection 0x%p for '%s'", dbInfo->mConnection.get(), NS_ConvertUTF16toUTF8(aDatabase->FilePath()).get())); #ifdef DEBUG dbInfo->mDEBUGConnectionThread = PR_GetCurrentThread(); #endif } dbInfo->AssertIsOnConnectionThread(); connection.forget(aConnection); return NS_OK; } uint64_t ConnectionPool::Start(const nsID& aBackgroundChildLoggingId, const nsACString& aDatabaseId, int64_t aLoggingSerialNumber, const nsTArray<nsString>& aObjectStoreNames, bool aIsWriteTransaction, TransactionDatabaseOperationBase* aTransactionOp) { AssertIsOnOwningThread(); MOZ_ASSERT(!aDatabaseId.IsEmpty()); MOZ_ASSERT(mNextTransactionId < UINT64_MAX); MOZ_ASSERT(!mShutdownRequested); PROFILER_LABEL("IndexedDB", "ConnectionPool::Start", js::ProfileEntry::Category::STORAGE); const uint64_t transactionId = ++mNextTransactionId; DatabaseInfo* dbInfo = mDatabases.Get(aDatabaseId); const bool databaseInfoIsNew = !dbInfo; if (databaseInfoIsNew) { dbInfo = new DatabaseInfo(this, aDatabaseId); MutexAutoLock lock(mDatabasesMutex); mDatabases.Put(aDatabaseId, dbInfo); } TransactionInfo* transactionInfo = new TransactionInfo(dbInfo, aBackgroundChildLoggingId, aDatabaseId, transactionId, aLoggingSerialNumber, aObjectStoreNames, aIsWriteTransaction, aTransactionOp); MOZ_ASSERT(!mTransactions.Get(transactionId)); mTransactions.Put(transactionId, transactionInfo); if (aIsWriteTransaction) { MOZ_ASSERT(dbInfo->mWriteTransactionCount < UINT32_MAX); dbInfo->mWriteTransactionCount++; } else { MOZ_ASSERT(dbInfo->mReadTransactionCount < UINT32_MAX); dbInfo->mReadTransactionCount++; } auto& blockingTransactions = dbInfo->mBlockingTransactions; for (uint32_t nameIndex = 0, nameCount = aObjectStoreNames.Length(); nameIndex < nameCount; nameIndex++) { const nsString& objectStoreName = aObjectStoreNames[nameIndex]; TransactionInfoPair* blockInfo = blockingTransactions.Get(objectStoreName); if (!blockInfo) { blockInfo = new TransactionInfoPair(); blockingTransactions.Put(objectStoreName, blockInfo); } // Mark what we are blocking on. if (TransactionInfo* blockingRead = blockInfo->mLastBlockingReads) { transactionInfo->mBlockedOn.PutEntry(blockingRead); blockingRead->AddBlockingTransaction(transactionInfo); } if (aIsWriteTransaction) { if (const uint32_t writeCount = blockInfo->mLastBlockingWrites.Length()) { for (uint32_t writeIndex = 0; writeIndex < writeCount; writeIndex++) { TransactionInfo* blockingWrite = blockInfo->mLastBlockingWrites[writeIndex]; MOZ_ASSERT(blockingWrite); transactionInfo->mBlockedOn.PutEntry(blockingWrite); blockingWrite->AddBlockingTransaction(transactionInfo); } } blockInfo->mLastBlockingReads = transactionInfo; blockInfo->mLastBlockingWrites.Clear(); } else { blockInfo->mLastBlockingWrites.AppendElement(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); PROFILER_LABEL("IndexedDB", "ConnectionPool::Dispatch", js::ProfileEntry::Category::STORAGE); TransactionInfo* transactionInfo = mTransactions.Get(aTransactionId); MOZ_ASSERT(transactionInfo); MOZ_ASSERT(!transactionInfo->mFinished); if (transactionInfo->mRunning) { DatabaseInfo* dbInfo = transactionInfo->mDatabaseInfo; MOZ_ASSERT(dbInfo); MOZ_ASSERT(dbInfo->mThreadInfo.mThread); MOZ_ASSERT(dbInfo->mThreadInfo.mRunnable); MOZ_ASSERT(!dbInfo->mClosing); MOZ_ASSERT_IF(transactionInfo->mIsWriteTransaction, dbInfo->mRunningWriteTransaction == transactionInfo); MOZ_ALWAYS_SUCCEEDS( dbInfo->mThreadInfo.mThread->Dispatch(aRunnable, NS_DISPATCH_NORMAL)); } else { transactionInfo->mQueuedRunnables.AppendElement(aRunnable); } } void ConnectionPool::Finish(uint64_t aTransactionId, FinishCallback* aCallback) { AssertIsOnOwningThread(); #ifdef DEBUG TransactionInfo* transactionInfo = mTransactions.Get(aTransactionId); MOZ_ASSERT(transactionInfo); MOZ_ASSERT(!transactionInfo->mFinished); #endif PROFILER_LABEL("IndexedDB", "ConnectionPool::Finish", js::ProfileEntry::Category::STORAGE); RefPtr<FinishCallbackWrapper> wrapper = new FinishCallbackWrapper(this, aTransactionId, aCallback); Dispatch(aTransactionId, wrapper); #ifdef DEBUG MOZ_ASSERT(!transactionInfo->mFinished); transactionInfo->mFinished = true; #endif } void ConnectionPool::WaitForDatabasesToComplete(nsTArray<nsCString>&& aDatabaseIds, nsIRunnable* aCallback) { AssertIsOnOwningThread(); MOZ_ASSERT(!aDatabaseIds.IsEmpty()); MOZ_ASSERT(aCallback); PROFILER_LABEL("IndexedDB", "ConnectionPool::WaitForDatabasesToComplete", js::ProfileEntry::Category::STORAGE); bool mayRunCallbackImmediately = true; for (uint32_t index = 0, count = aDatabaseIds.Length(); index < count; index++) { const nsCString& databaseId = aDatabaseIds[index]; MOZ_ASSERT(!databaseId.IsEmpty()); if (CloseDatabaseWhenIdleInternal(databaseId)) { mayRunCallbackImmediately = false; } } if (mayRunCallbackImmediately) { Unused << aCallback->Run(); return; } nsAutoPtr<DatabasesCompleteCallback> callback( new DatabasesCompleteCallback(Move(aDatabaseIds), aCallback)); mCompleteCallbacks.AppendElement(callback.forget()); } void ConnectionPool::Shutdown() { AssertIsOnOwningThread(); MOZ_ASSERT(!mShutdownRequested); MOZ_ASSERT(!mShutdownComplete); PROFILER_LABEL("IndexedDB", "ConnectionPool::Shutdown", js::ProfileEntry::Category::STORAGE); mShutdownRequested = true; CancelIdleTimer(); MOZ_ASSERT(mTargetIdleTime.IsNull()); mIdleTimer = nullptr; CloseIdleDatabases(); ShutdownIdleThreads(); if (!mDatabases.Count()) { MOZ_ASSERT(!mTransactions.Count()); Cleanup(); MOZ_ASSERT(mShutdownComplete); return; } nsIThread* currentThread = NS_GetCurrentThread(); MOZ_ASSERT(currentThread); while (!mShutdownComplete) { MOZ_ALWAYS_TRUE(NS_ProcessNextEvent(currentThread)); } } void ConnectionPool::Cleanup() { AssertIsOnOwningThread(); MOZ_ASSERT(mShutdownRequested); MOZ_ASSERT(!mShutdownComplete); MOZ_ASSERT(!mDatabases.Count()); MOZ_ASSERT(!mTransactions.Count()); MOZ_ASSERT(mIdleThreads.IsEmpty()); PROFILER_LABEL("IndexedDB", "ConnectionPool::Cleanup", js::ProfileEntry::Category::STORAGE); if (!mCompleteCallbacks.IsEmpty()) { // Run all callbacks manually now. for (uint32_t count = mCompleteCallbacks.Length(), index = 0; index < count; index++) { nsAutoPtr<DatabasesCompleteCallback> completeCallback( mCompleteCallbacks[index].forget()); MOZ_ASSERT(completeCallback); MOZ_ASSERT(completeCallback->mCallback); Unused << completeCallback->mCallback->Run(); } mCompleteCallbacks.Clear(); // And make sure they get processed. nsIThread* currentThread = NS_GetCurrentThread(); MOZ_ASSERT(currentThread); MOZ_ALWAYS_SUCCEEDS(NS_ProcessPendingEvents(currentThread)); } mShutdownComplete = true; } void ConnectionPool::AdjustIdleTimer() { AssertIsOnOwningThread(); MOZ_ASSERT(mIdleTimer); PROFILER_LABEL("IndexedDB", "ConnectionPool::AdjustIdleTimer", js::ProfileEntry::Category::STORAGE); // 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->InitWithFuncCallback(IdleTimerCallback, this, delay, nsITimer::TYPE_ONE_SHOT)); 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(aThreadInfo.mThread); MOZ_ASSERT(aThreadInfo.mRunnable); MOZ_ASSERT(mTotalThreadCount); RefPtr<ThreadRunnable> runnable; aThreadInfo.mRunnable.swap(runnable); nsCOMPtr<nsIThread> thread; aThreadInfo.mThread.swap(thread); IDB_DEBUG_LOG(("ConnectionPool shutting down thread %lu", 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(thread, &nsIThread::Shutdown))); mTotalThreadCount--; } void ConnectionPool::CloseIdleDatabases() { AssertIsOnOwningThread(); MOZ_ASSERT(mShutdownRequested); PROFILER_LABEL("IndexedDB", "ConnectionPool::CloseIdleDatabases", js::ProfileEntry::Category::STORAGE); if (!mIdleDatabases.IsEmpty()) { for (IdleDatabaseInfo& idleInfo : mIdleDatabases) { CloseDatabase(idleInfo.mDatabaseInfo); } mIdleDatabases.Clear(); } if (!mDatabasesPerformingIdleMaintenance.IsEmpty()) { for (DatabaseInfo* dbInfo : mDatabasesPerformingIdleMaintenance) { MOZ_ASSERT(dbInfo); CloseDatabase(dbInfo); } mDatabasesPerformingIdleMaintenance.Clear(); } } void ConnectionPool::ShutdownIdleThreads() { AssertIsOnOwningThread(); MOZ_ASSERT(mShutdownRequested); PROFILER_LABEL("IndexedDB", "ConnectionPool::ShutdownIdleThreads", js::ProfileEntry::Category::STORAGE); if (!mIdleThreads.IsEmpty()) { for (uint32_t threadCount = mIdleThreads.Length(), threadIndex = 0; threadIndex < threadCount; threadIndex++) { ShutdownThread(mIdleThreads[threadIndex].mThreadInfo); } mIdleThreads.Clear(); } } bool ConnectionPool::ScheduleTransaction(TransactionInfo* aTransactionInfo, bool aFromQueuedTransactions) { AssertIsOnOwningThread(); MOZ_ASSERT(aTransactionInfo); PROFILER_LABEL("IndexedDB", "ConnectionPool::ScheduleTransaction", js::ProfileEntry::Category::STORAGE); DatabaseInfo* dbInfo = aTransactionInfo->mDatabaseInfo; MOZ_ASSERT(dbInfo); dbInfo->mIdle = false; if (dbInfo->mClosing) { MOZ_ASSERT(!mIdleDatabases.Contains(dbInfo)); MOZ_ASSERT( !dbInfo->mTransactionsScheduledDuringClose.Contains(aTransactionInfo)); dbInfo->mTransactionsScheduledDuringClose.AppendElement(aTransactionInfo); return true; } if (!dbInfo->mThreadInfo.mThread) { MOZ_ASSERT(!dbInfo->mThreadInfo.mRunnable); if (mIdleThreads.IsEmpty()) { bool created = false; if (mTotalThreadCount < kMaxConnectionThreadCount) { // This will set the thread up with the profiler. RefPtr<ThreadRunnable> runnable = new ThreadRunnable(); nsCOMPtr<nsIThread> newThread; if (NS_SUCCEEDED(NS_NewThread(getter_AddRefs(newThread), runnable))) { MOZ_ASSERT(newThread); IDB_DEBUG_LOG(("ConnectionPool created thread %lu", runnable->SerialNumber())); dbInfo->mThreadInfo.mThread.swap(newThread); dbInfo->mThreadInfo.mRunnable.swap(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. nsCOMPtr<nsIRunnable> runnable = new Runnable(); for (uint32_t index = mDatabasesPerformingIdleMaintenance.Length(); index > 0; index--) { DatabaseInfo* dbInfo = mDatabasesPerformingIdleMaintenance[index - 1]; MOZ_ASSERT(dbInfo); MOZ_ASSERT(dbInfo->mThreadInfo.mThread); MOZ_ALWAYS_SUCCEEDS( dbInfo->mThreadInfo.mThread->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL)); } } if (!created) { if (!aFromQueuedTransactions) { MOZ_ASSERT(!mQueuedTransactions.Contains(aTransactionInfo)); mQueuedTransactions.AppendElement(aTransactionInfo); } return false; } } else { const uint32_t lastIndex = mIdleThreads.Length() - 1; ThreadInfo& threadInfo = mIdleThreads[lastIndex].mThreadInfo; dbInfo->mThreadInfo.mRunnable.swap(threadInfo.mRunnable); dbInfo->mThreadInfo.mThread.swap(threadInfo.mThread); mIdleThreads.RemoveElementAt(lastIndex); AdjustIdleTimer(); } } MOZ_ASSERT(dbInfo->mThreadInfo.mThread); MOZ_ASSERT(dbInfo->mThreadInfo.mRunnable); 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(aTransactionInfo); return true; } dbInfo->mRunningWriteTransaction = aTransactionInfo; dbInfo->mNeedsCheckpoint = true; } MOZ_ASSERT(!aTransactionInfo->mRunning); aTransactionInfo->mRunning = true; nsTArray<nsCOMPtr<nsIRunnable>>& queuedRunnables = aTransactionInfo->mQueuedRunnables; if (!queuedRunnables.IsEmpty()) { for (uint32_t index = 0, count = queuedRunnables.Length(); index < count; index++) { nsCOMPtr<nsIRunnable> runnable; queuedRunnables[index].swap(runnable); MOZ_ALWAYS_SUCCEEDS( dbInfo->mThreadInfo.mThread->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL)); } queuedRunnables.Clear(); } return true; } void ConnectionPool::NoteFinishedTransaction(uint64_t aTransactionId) { AssertIsOnOwningThread(); PROFILER_LABEL("IndexedDB", "ConnectionPool::NoteFinishedTransaction", js::ProfileEntry::Category::STORAGE); TransactionInfo* transactionInfo = mTransactions.Get(aTransactionId); MOZ_ASSERT(transactionInfo); MOZ_ASSERT(transactionInfo->mRunning); MOZ_ASSERT(transactionInfo->mFinished); transactionInfo->mRunning = false; DatabaseInfo* dbInfo = transactionInfo->mDatabaseInfo; MOZ_ASSERT(dbInfo); MOZ_ASSERT(mDatabases.Get(transactionInfo->mDatabaseId) == dbInfo); MOZ_ASSERT(dbInfo->mThreadInfo.mThread); MOZ_ASSERT(dbInfo->mThreadInfo.mRunnable); // Schedule the next write transaction if there are any queued. if (dbInfo->mRunningWriteTransaction == transactionInfo) { MOZ_ASSERT(transactionInfo->mIsWriteTransaction); MOZ_ASSERT(dbInfo->mNeedsCheckpoint); dbInfo->mRunningWriteTransaction = nullptr; if (!dbInfo->mScheduledWriteTransactions.IsEmpty()) { TransactionInfo* nextWriteTransaction = dbInfo->mScheduledWriteTransactions[0]; MOZ_ASSERT(nextWriteTransaction); dbInfo->mScheduledWriteTransactions.RemoveElementAt(0); MOZ_ALWAYS_TRUE(ScheduleTransaction(nextWriteTransaction, /* aFromQueuedTransactions */ false)); } } const nsTArray<nsString>& objectStoreNames = transactionInfo->mObjectStoreNames; for (uint32_t index = 0, count = objectStoreNames.Length(); index < count; index++) { TransactionInfoPair* blockInfo = dbInfo->mBlockingTransactions.Get(objectStoreNames[index]); MOZ_ASSERT(blockInfo); if (transactionInfo->mIsWriteTransaction && blockInfo->mLastBlockingReads == transactionInfo) { blockInfo->mLastBlockingReads = nullptr; } 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); #ifdef DEBUG // That just deleted |transactionInfo|. transactionInfo = nullptr; #endif if (!dbInfo->TotalTransactionCount()) { MOZ_ASSERT(!dbInfo->mIdle); dbInfo->mIdle = true; NoteIdleDatabase(dbInfo); } } void ConnectionPool::ScheduleQueuedTransactions(ThreadInfo& aThreadInfo) { AssertIsOnOwningThread(); MOZ_ASSERT(aThreadInfo.mThread); MOZ_ASSERT(aThreadInfo.mRunnable); MOZ_ASSERT(!mQueuedTransactions.IsEmpty()); MOZ_ASSERT(!mIdleThreads.Contains(aThreadInfo)); PROFILER_LABEL("IndexedDB", "ConnectionPool::ScheduleQueuedTransactions", js::ProfileEntry::Category::STORAGE); mIdleThreads.InsertElementSorted(aThreadInfo); aThreadInfo.mRunnable = nullptr; aThreadInfo.mThread = nullptr; uint32_t index = 0; for (uint32_t count = mQueuedTransactions.Length(); index < count; index++) { if (!ScheduleTransaction(mQueuedTransactions[index], /* aFromQueuedTransactions */ true)) { break; } } if (index) { mQueuedTransactions.RemoveElementsAt(0, index); } AdjustIdleTimer(); } void ConnectionPool::NoteIdleDatabase(DatabaseInfo* aDatabaseInfo) { AssertIsOnOwningThread(); MOZ_ASSERT(aDatabaseInfo); MOZ_ASSERT(!aDatabaseInfo->TotalTransactionCount()); MOZ_ASSERT(aDatabaseInfo->mThreadInfo.mThread); MOZ_ASSERT(aDatabaseInfo->mThreadInfo.mRunnable); MOZ_ASSERT(!mIdleDatabases.Contains(aDatabaseInfo)); PROFILER_LABEL("IndexedDB", "ConnectionPool::NoteIdleDatabase", js::ProfileEntry::Category::STORAGE); 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(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(aDatabaseInfo->mThreadInfo); } return; } mIdleDatabases.InsertElementSorted(aDatabaseInfo); AdjustIdleTimer(); } void ConnectionPool::NoteClosedDatabase(DatabaseInfo* aDatabaseInfo) { AssertIsOnOwningThread(); MOZ_ASSERT(aDatabaseInfo); MOZ_ASSERT(aDatabaseInfo->mClosing); MOZ_ASSERT(!mIdleDatabases.Contains(aDatabaseInfo)); PROFILER_LABEL("IndexedDB", "ConnectionPool::NoteClosedDatabase", js::ProfileEntry::Category::STORAGE); 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.mThread) { MOZ_ASSERT(aDatabaseInfo->mThreadInfo.mRunnable); if (!mQueuedTransactions.IsEmpty()) { // Give the thread to another database. ScheduleQueuedTransactions(aDatabaseInfo->mThreadInfo); } else if (!aDatabaseInfo->TotalTransactionCount()) { if (mShutdownRequested) { ShutdownThread(aDatabaseInfo->mThreadInfo); } else { MOZ_ASSERT(!mIdleThreads.Contains(aDatabaseInfo->mThreadInfo)); mIdleThreads.InsertElementSorted(aDatabaseInfo->mThreadInfo); aDatabaseInfo->mThreadInfo.mRunnable = nullptr; aDatabaseInfo->mThreadInfo.mThread = nullptr; if (mIdleThreads.Length() > kMaxIdleConnectionThreadCount) { ShutdownThread(mIdleThreads[0].mThreadInfo); mIdleThreads.RemoveElementAt(0); } AdjustIdleTimer(); } } } // Schedule any transactions that were started while we were closing the // connection. if (aDatabaseInfo->TotalTransactionCount()) { nsTArray<TransactionInfo*>& scheduledTransactions = aDatabaseInfo->mTransactionsScheduledDuringClose; MOZ_ASSERT(!scheduledTransactions.IsEmpty()); for (uint32_t index = 0, count = scheduledTransactions.Length(); index < count; index++) { Unused << ScheduleTransaction(scheduledTransactions[index], /* 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); } #ifdef DEBUG // That just deleted |aDatabaseInfo|. aDatabaseInfo = nullptr; #endif // See if we need to fire any complete callbacks now that the database is // finished. for (uint32_t index = 0; index < mCompleteCallbacks.Length(); /* conditionally incremented */) { if (MaybeFireCallback(mCompleteCallbacks[index])) { mCompleteCallbacks.RemoveElementAt(index); } else { index++; } } // 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(DatabasesCompleteCallback* aCallback) { AssertIsOnOwningThread(); MOZ_ASSERT(aCallback); MOZ_ASSERT(!aCallback->mDatabaseIds.IsEmpty()); MOZ_ASSERT(aCallback->mCallback); PROFILER_LABEL("IndexedDB", "ConnectionPool::MaybeFireCallback", js::ProfileEntry::Category::STORAGE); for (uint32_t count = aCallback->mDatabaseIds.Length(), index = 0; index < count; index++) { const nsCString& databaseId = aCallback->mDatabaseIds[index]; MOZ_ASSERT(!databaseId.IsEmpty()); if (mDatabases.Get(databaseId)) { return false; } } Unused << aCallback->mCallback->Run(); return true; } void ConnectionPool::PerformIdleDatabaseMaintenance(DatabaseInfo* aDatabaseInfo) { AssertIsOnOwningThread(); MOZ_ASSERT(aDatabaseInfo); MOZ_ASSERT(!aDatabaseInfo->TotalTransactionCount()); MOZ_ASSERT(aDatabaseInfo->mThreadInfo.mThread); MOZ_ASSERT(aDatabaseInfo->mThreadInfo.mRunnable); MOZ_ASSERT(aDatabaseInfo->mIdle); MOZ_ASSERT(!aDatabaseInfo->mCloseOnIdle); MOZ_ASSERT(!aDatabaseInfo->mClosing); MOZ_ASSERT(mIdleDatabases.Contains(aDatabaseInfo)); MOZ_ASSERT(!mDatabasesPerformingIdleMaintenance.Contains(aDatabaseInfo)); nsCOMPtr<nsIRunnable> runnable = new IdleConnectionRunnable(aDatabaseInfo, aDatabaseInfo->mNeedsCheckpoint); aDatabaseInfo->mNeedsCheckpoint = false; aDatabaseInfo->mIdle = false; mDatabasesPerformingIdleMaintenance.AppendElement(aDatabaseInfo); MOZ_ALWAYS_SUCCEEDS( aDatabaseInfo->mThreadInfo.mThread->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL)); } void ConnectionPool::CloseDatabase(DatabaseInfo* aDatabaseInfo) { AssertIsOnOwningThread(); MOZ_ASSERT(aDatabaseInfo); MOZ_ASSERT(!aDatabaseInfo->TotalTransactionCount()); MOZ_ASSERT(aDatabaseInfo->mThreadInfo.mThread); MOZ_ASSERT(aDatabaseInfo->mThreadInfo.mRunnable); MOZ_ASSERT(!aDatabaseInfo->mClosing); aDatabaseInfo->mIdle = false; aDatabaseInfo->mNeedsCheckpoint = false; aDatabaseInfo->mClosing = true; nsCOMPtr<nsIRunnable> runnable = new CloseConnectionRunnable(aDatabaseInfo); MOZ_ALWAYS_SUCCEEDS( aDatabaseInfo->mThreadInfo.mThread->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL)); } bool ConnectionPool::CloseDatabaseWhenIdleInternal(const nsACString& aDatabaseId) { AssertIsOnOwningThread(); MOZ_ASSERT(!aDatabaseId.IsEmpty()); PROFILER_LABEL("IndexedDB", "ConnectionPool::CloseDatabaseWhenIdleInternal", js::ProfileEntry::Category::STORAGE); if (DatabaseInfo* dbInfo = mDatabases.Get(aDatabaseId)) { if (mIdleDatabases.RemoveElement(dbInfo) || mDatabasesPerformingIdleMaintenance.RemoveElement(dbInfo)) { CloseDatabase(dbInfo); AdjustIdleTimer(); } else { dbInfo->mCloseOnIdle = true; } return true; } return false; } ConnectionPool:: ConnectionRunnable::ConnectionRunnable(DatabaseInfo* aDatabaseInfo) : mDatabaseInfo(aDatabaseInfo) , mOwningThread(do_GetCurrentThread()) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aDatabaseInfo); MOZ_ASSERT(aDatabaseInfo->mConnectionPool); aDatabaseInfo->mConnectionPool->AssertIsOnOwningThread(); MOZ_ASSERT(mOwningThread); } NS_IMPL_ISUPPORTS_INHERITED0(ConnectionPool::IdleConnectionRunnable, ConnectionPool::ConnectionRunnable) NS_IMETHODIMP ConnectionPool:: IdleConnectionRunnable::Run() { MOZ_ASSERT(mDatabaseInfo); MOZ_ASSERT(!mDatabaseInfo->mIdle); nsCOMPtr<nsIEventTarget> owningThread; mOwningThread.swap(owningThread); 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; } } RefPtr<ConnectionPool> 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_IMPL_ISUPPORTS_INHERITED0(ConnectionPool::CloseConnectionRunnable, ConnectionPool::ConnectionRunnable) NS_IMETHODIMP ConnectionPool:: CloseConnectionRunnable::Run() { MOZ_ASSERT(mDatabaseInfo); PROFILER_LABEL("IndexedDB", "ConnectionPool::CloseConnectionRunnable::Run", js::ProfileEntry::Category::STORAGE); if (mOwningThread) { MOZ_ASSERT(mDatabaseInfo->mClosing); nsCOMPtr<nsIEventTarget> owningThread; mOwningThread.swap(owningThread); // 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->mDEBUGConnectionThread = nullptr; #endif } MOZ_ALWAYS_SUCCEEDS( owningThread->Dispatch(this, NS_DISPATCH_NORMAL)); return NS_OK; } RefPtr<ConnectionPool> connectionPool = mDatabaseInfo->mConnectionPool; MOZ_ASSERT(connectionPool); connectionPool->NoteClosedDatabase(mDatabaseInfo); return NS_OK; } ConnectionPool:: DatabaseInfo::DatabaseInfo(ConnectionPool* aConnectionPool, const nsACString& aDatabaseId) : mConnectionPool(aConnectionPool) , mDatabaseId(aDatabaseId) , mRunningWriteTransaction(nullptr) , mReadTransactionCount(0) , mWriteTransactionCount(0) , mNeedsCheckpoint(false) , mIdle(false) , mCloseOnIdle(false) , mClosing(false) #ifdef DEBUG , mDEBUGConnectionThread(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); MOZ_ASSERT(!mThreadInfo.mThread); MOZ_ASSERT(!mThreadInfo.mRunnable); MOZ_ASSERT(!TotalTransactionCount()); MOZ_COUNT_DTOR(ConnectionPool::DatabaseInfo); } ConnectionPool:: DatabasesCompleteCallback::DatabasesCompleteCallback( nsTArray<nsCString>&& aDatabaseIds, nsIRunnable* aCallback) : mDatabaseIds(Move(aDatabaseIds)) , mCallback(aCallback) { AssertIsOnBackgroundThread(); MOZ_ASSERT(!mDatabaseIds.IsEmpty()); MOZ_ASSERT(aCallback); MOZ_COUNT_CTOR(ConnectionPool::DatabasesCompleteCallback); } ConnectionPool:: DatabasesCompleteCallback::~DatabasesCompleteCallback() { AssertIsOnBackgroundThread(); MOZ_COUNT_DTOR(ConnectionPool::DatabasesCompleteCallback); } ConnectionPool:: FinishCallbackWrapper::FinishCallbackWrapper(ConnectionPool* aConnectionPool, uint64_t aTransactionId, FinishCallback* aCallback) : mConnectionPool(aConnectionPool) , mCallback(aCallback) , mOwningThread(do_GetCurrentThread()) , mTransactionId(aTransactionId) , mHasRunOnce(false) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aConnectionPool); MOZ_ASSERT(aCallback); MOZ_ASSERT(mOwningThread); } ConnectionPool:: FinishCallbackWrapper::~FinishCallbackWrapper() { MOZ_ASSERT(!mConnectionPool); MOZ_ASSERT(!mCallback); } NS_IMPL_ISUPPORTS_INHERITED0(ConnectionPool::FinishCallbackWrapper, Runnable) nsresult ConnectionPool:: FinishCallbackWrapper::Run() { MOZ_ASSERT(mConnectionPool); MOZ_ASSERT(mCallback); MOZ_ASSERT(mOwningThread); PROFILER_LABEL("IndexedDB", "ConnectionPool::FinishCallbackWrapper::Run", js::ProfileEntry::Category::STORAGE); if (!mHasRunOnce) { MOZ_ASSERT(!IsOnBackgroundThread()); mHasRunOnce = true; Unused << mCallback->Run(); MOZ_ALWAYS_SUCCEEDS( mOwningThread->Dispatch(this, NS_DISPATCH_NORMAL)); return NS_OK; } mConnectionPool->AssertIsOnOwningThread(); MOZ_ASSERT(mHasRunOnce); RefPtr<ConnectionPool> connectionPool = Move(mConnectionPool); RefPtr<FinishCallback> callback = Move(mCallback); callback->TransactionFinishedBeforeUnblock(); connectionPool->NoteFinishedTransaction(mTransactionId); callback->TransactionFinishedAfterUnblock(); return NS_OK; } uint32_t ConnectionPool::ThreadRunnable::sNextSerialNumber = 0; ConnectionPool:: ThreadRunnable::ThreadRunnable() : mSerialNumber(++sNextSerialNumber) , mFirstRun(true) , mContinueRunning(true) { AssertIsOnBackgroundThread(); } ConnectionPool:: ThreadRunnable::~ThreadRunnable() { MOZ_ASSERT(!mFirstRun); MOZ_ASSERT(!mContinueRunning); } NS_IMPL_ISUPPORTS_INHERITED0(ConnectionPool::ThreadRunnable, Runnable) nsresult ConnectionPool:: ThreadRunnable::Run() { #ifdef MOZ_ENABLE_PROFILER_SPS char stackTopGuess; #endif // MOZ_ENABLE_PROFILER_SPS MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(mContinueRunning); if (!mFirstRun) { mContinueRunning = false; return NS_OK; } mFirstRun = false; { // Scope for the thread name. Both PR_SetCurrentThreadName() and // profiler_register_thread() copy the string so we don't need to keep it. const nsPrintfCString threadName("IndexedDB #%lu", mSerialNumber); PR_SetCurrentThreadName(threadName.get()); #ifdef MOZ_ENABLE_PROFILER_SPS profiler_register_thread(threadName.get(), &stackTopGuess); #endif // MOZ_ENABLE_PROFILER_SPS } { // Scope for the profiler label. PROFILER_LABEL("IndexedDB", "ConnectionPool::ThreadRunnable::Run", js::ProfileEntry::Category::STORAGE); nsIThread* currentThread = NS_GetCurrentThread(); MOZ_ASSERT(currentThread); #ifdef DEBUG if (kDEBUGTransactionThreadPriority != nsISupportsPriority::PRIORITY_NORMAL) { NS_WARNING("ConnectionPool thread debugging enabled, priority has been " "modified!"); nsCOMPtr<nsISupportsPriority> 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 while (mContinueRunning) { MOZ_ALWAYS_TRUE(NS_ProcessNextEvent(currentThread)); #ifdef DEBUG if (kDEBUGTransactionThreadSleepMS) { MOZ_ALWAYS_TRUE( PR_Sleep(PR_MillisecondsToInterval(kDEBUGTransactionThreadSleepMS)) == PR_SUCCESS); } #endif // DEBUG } } #ifdef MOZ_ENABLE_PROFILER_SPS profiler_unregister_thread(); #endif // MOZ_ENABLE_PROFILER_SPS return NS_OK; } ConnectionPool:: ThreadInfo::ThreadInfo() { AssertIsOnBackgroundThread(); MOZ_COUNT_CTOR(ConnectionPool::ThreadInfo); } ConnectionPool:: ThreadInfo::ThreadInfo(const ThreadInfo& aOther) : mThread(aOther.mThread) , mRunnable(aOther.mRunnable) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aOther.mThread); MOZ_ASSERT(aOther.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(aDatabaseInfo) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aDatabaseInfo); MOZ_COUNT_CTOR(ConnectionPool::IdleDatabaseInfo); } ConnectionPool:: IdleDatabaseInfo::~IdleDatabaseInfo() { AssertIsOnBackgroundThread(); MOZ_ASSERT(mDatabaseInfo); MOZ_COUNT_DTOR(ConnectionPool::IdleDatabaseInfo); } ConnectionPool:: IdleThreadInfo::IdleThreadInfo(const ThreadInfo& aThreadInfo) : IdleResource(TimeStamp::NowLoRes() + TimeDuration::FromMilliseconds(kConnectionThreadIdleMS)) , mThreadInfo(aThreadInfo) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aThreadInfo.mRunnable); MOZ_ASSERT(aThreadInfo.mThread); 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<nsString>& aObjectStoreNames, bool aIsWriteTransaction, TransactionDatabaseOperationBase* aTransactionOp) : mDatabaseInfo(aDatabaseInfo) , mBackgroundChildLoggingId(aBackgroundChildLoggingId) , mDatabaseId(aDatabaseId) , mTransactionId(aTransactionId) , mLoggingSerialNumber(aLoggingSerialNumber) , mObjectStoreNames(aObjectStoreNames) , mIsWriteTransaction(aIsWriteTransaction) , mRunning(false) #ifdef DEBUG , mFinished(false) #endif { AssertIsOnBackgroundThread(); MOZ_ASSERT(aDatabaseInfo); 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(); MOZ_ASSERT(aTransactionInfo); if (!mBlocking.Contains(aTransactionInfo)) { mBlocking.PutEntry(aTransactionInfo); mBlockingOrdered.AppendElement(aTransactionInfo); } } void ConnectionPool:: TransactionInfo::RemoveBlockingTransactions() { AssertIsOnBackgroundThread(); for (uint32_t index = 0, count = mBlockingOrdered.Length(); index < count; index++) { TransactionInfo* blockedInfo = mBlockingOrdered[index]; MOZ_ASSERT(blockedInfo); blockedInfo->MaybeUnblock(this); } mBlocking.Clear(); mBlockingOrdered.Clear(); } void ConnectionPool:: TransactionInfo::MaybeUnblock(TransactionInfo* aTransactionInfo) { AssertIsOnBackgroundThread(); MOZ_ASSERT(mBlockedOn.Contains(aTransactionInfo)); mBlockedOn.RemoveEntry(aTransactionInfo); if (!mBlockedOn.Count()) { MOZ_ASSERT(mDatabaseInfo); ConnectionPool* connectionPool = mDatabaseInfo->mConnectionPool; MOZ_ASSERT(connectionPool); connectionPool->AssertIsOnOwningThread(); Unused << connectionPool->ScheduleTransaction(this, /* aFromQueuedTransactions */ false); } } ConnectionPool:: TransactionInfoPair::TransactionInfoPair() : mLastBlockingReads(nullptr) { AssertIsOnBackgroundThread(); MOZ_COUNT_CTOR(ConnectionPool::TransactionInfoPair); } ConnectionPool:: TransactionInfoPair::~TransactionInfoPair() { AssertIsOnBackgroundThread(); MOZ_COUNT_DTOR(ConnectionPool::TransactionInfoPair); } /******************************************************************************* * Metadata classes ******************************************************************************/ bool FullObjectStoreMetadata::HasLiveIndexes() const { AssertIsOnBackgroundThread(); for (auto iter = mIndexes.ConstIter(); !iter.Done(); iter.Next()) { if (!iter.Data()->mDeleted) { return true; } } return false; } already_AddRefed<FullDatabaseMetadata> 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. RefPtr<FullDatabaseMetadata> newMetadata = new FullDatabaseMetadata(mCommonMetadata); newMetadata->mDatabaseId = mDatabaseId; newMetadata->mFilePath = mFilePath; newMetadata->mNextObjectStoreId = mNextObjectStoreId; newMetadata->mNextIndexId = mNextIndexId; for (auto iter = mObjectStores.ConstIter(); !iter.Done(); iter.Next()) { auto key = iter.Key(); auto value = iter.Data(); RefPtr<FullObjectStoreMetadata> newOSMetadata = new FullObjectStoreMetadata(); newOSMetadata->mCommonMetadata = value->mCommonMetadata; newOSMetadata->mNextAutoIncrementId = value->mNextAutoIncrementId; newOSMetadata->mCommittedAutoIncrementId = value->mCommittedAutoIncrementId; for (auto iter = value->mIndexes.ConstIter(); !iter.Done(); iter.Next()) { auto key = iter.Key(); auto value = iter.Data(); RefPtr<FullIndexMetadata> newIndexMetadata = new FullIndexMetadata(); newIndexMetadata->mCommonMetadata = value->mCommonMetadata; if (NS_WARN_IF(!newOSMetadata->mIndexes.Put(key, newIndexMetadata, fallible))) { return nullptr; } } MOZ_ASSERT(value->mIndexes.Count() == newOSMetadata->mIndexes.Count()); if (NS_WARN_IF(!newMetadata->mObjectStores.Put(key, newOSMetadata, fallible))) { return nullptr; } } MOZ_ASSERT(mObjectStores.Count() == newMetadata->mObjectStores.Count()); return newMetadata.forget(); } DatabaseLoggingInfo::~DatabaseLoggingInfo() { AssertIsOnBackgroundThread(); if (gLoggingInfoHashtable) { const nsID& backgroundChildLoggingId = mLoggingInfo.backgroundChildLoggingId(); MOZ_ASSERT(gLoggingInfoHashtable->Get(backgroundChildLoggingId) == this); gLoggingInfoHashtable->Remove(backgroundChildLoggingId); } } /******************************************************************************* * Factory ******************************************************************************/ Factory::Factory(already_AddRefed<DatabaseLoggingInfo> aLoggingInfo) : mLoggingInfo(Move(aLoggingInfo)) #ifdef DEBUG , mActorDestroyed(false) #endif { AssertIsOnBackgroundThread(); MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); } Factory::~Factory() { MOZ_ASSERT(mActorDestroyed); } // static already_AddRefed<Factory> Factory::Create(const LoggingInfo& aLoggingInfo) { AssertIsOnBackgroundThread(); MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); // Balanced in ActoryDestroy(). IncreaseBusyCount(); MOZ_ASSERT(gLoggingInfoHashtable); RefPtr<DatabaseLoggingInfo> loggingInfo = gLoggingInfoHashtable->Get(aLoggingInfo.backgroundChildLoggingId()); if (loggingInfo) { MOZ_ASSERT(aLoggingInfo.backgroundChildLoggingId() == loggingInfo->Id()); #if !DISABLE_ASSERTS_FOR_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 // !DISABLE_ASSERTS_FOR_FUZZING } else { loggingInfo = new DatabaseLoggingInfo(aLoggingInfo); gLoggingInfoHashtable->Put(aLoggingInfo.backgroundChildLoggingId(), loggingInfo); } RefPtr<Factory> actor = new Factory(loggingInfo.forget()); return actor.forget(); } void Factory::ActorDestroy(ActorDestroyReason aWhy) { AssertIsOnBackgroundThread(); MOZ_ASSERT(!mActorDestroyed); #ifdef DEBUG mActorDestroyed = true; #endif // Match the IncreaseBusyCount in Create(). DecreaseBusyCount(); } bool Factory::RecvDeleteMe() { AssertIsOnBackgroundThread(); MOZ_ASSERT(!mActorDestroyed); return PBackgroundIDBFactoryParent::Send__delete__(this); } bool Factory::RecvIncrementLoggingRequestSerialNumber() { AssertIsOnBackgroundThread(); MOZ_ASSERT(mLoggingInfo); mLoggingInfo->NextRequestSN(); return true; } 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_WARN_IF(metadata.persistenceType() != PERSISTENCE_TYPE_PERSISTENT && metadata.persistenceType() != PERSISTENCE_TYPE_TEMPORARY && metadata.persistenceType() != PERSISTENCE_TYPE_DEFAULT)) { ASSERT_UNLESS_FUZZING(); return nullptr; } const PrincipalInfo& principalInfo = commonParams->principalInfo(); if (NS_WARN_IF(principalInfo.type() == PrincipalInfo::TNullPrincipalInfo)) { ASSERT_UNLESS_FUZZING(); return nullptr; } if (NS_WARN_IF(principalInfo.type() == PrincipalInfo::TSystemPrincipalInfo && metadata.persistenceType() != PERSISTENCE_TYPE_PERSISTENT)) { ASSERT_UNLESS_FUZZING(); return nullptr; } RefPtr<ContentParent> contentParent = BackgroundParent::GetContentParent(Manager()); RefPtr<FactoryOp> actor; if (aParams.type() == FactoryRequestParams::TOpenDatabaseRequestParams) { actor = new OpenDatabaseOp(this, contentParent.forget(), *commonParams); } else { actor = new DeleteDatabaseOp(this, contentParent.forget(), *commonParams); } // Transfer ownership to IPDL. return actor.forget().take(); } bool 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<FactoryOp*>(aActor); MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(op)); return true; } bool Factory::DeallocPBackgroundIDBFactoryRequestParent( PBackgroundIDBFactoryRequestParent* aActor) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); // Transfer ownership back from IPDL. RefPtr<FactoryOp> op = dont_AddRef(static_cast<FactoryOp*>(aActor)); return true; } PBackgroundIDBDatabaseParent* Factory::AllocPBackgroundIDBDatabaseParent( const DatabaseSpec& aSpec, PBackgroundIDBFactoryRequestParent* aRequest) { MOZ_CRASH("PBackgroundIDBDatabaseParent actors should be constructed " "manually!"); } bool Factory::DeallocPBackgroundIDBDatabaseParent( PBackgroundIDBDatabaseParent* aActor) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); RefPtr<Database> database = dont_AddRef(static_cast<Database*>(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> connectionPool = gConnectionPool.get(); if (connectionPool) { nsTArray<nsCString> ids(1); ids.AppendElement(mDatabaseId); mState = State::WaitingForTransactions; connectionPool->WaitForDatabasesToComplete(Move(ids), this); return; } MaybeWaitForFileHandles(); } void WaitForTransactionsHelper::MaybeWaitForFileHandles() { AssertIsOnBackgroundThread(); MOZ_ASSERT(mState == State::Initial || mState == State::WaitingForTransactions); RefPtr<FileHandleThreadPool> fileHandleThreadPool = gFileHandleThreadPool.get(); if (fileHandleThreadPool) { nsTArray<nsCString> ids(1); ids.AppendElement(mDatabaseId); mState = State::WaitingForFileHandles; fileHandleThreadPool->WaitForDirectoriesToComplete(Move(ids), this); return; } CallCallback(); } void WaitForTransactionsHelper::CallCallback() { AssertIsOnBackgroundThread(); MOZ_ASSERT(mState == State::Initial || mState == State::WaitingForTransactions || mState == State::WaitingForFileHandles); nsCOMPtr<nsIRunnable> callback; mCallback.swap(callback); callback->Run(); mState = State::Complete; } NS_IMPL_ISUPPORTS_INHERITED0(WaitForTransactionsHelper, Runnable) NS_IMETHODIMP WaitForTransactionsHelper::Run() { MOZ_ASSERT(mState != State::Complete); MOZ_ASSERT(mCallback); switch (mState) { case State::Initial: MaybeWaitForTransactions(); break; case State::WaitingForTransactions: MaybeWaitForFileHandles(); break; case State::WaitingForFileHandles: CallCallback(); break; default: MOZ_CRASH("Should never get here!"); } return NS_OK; } /******************************************************************************* * Database ******************************************************************************/ Database::Database(Factory* aFactory, const PrincipalInfo& aPrincipalInfo, const Maybe<ContentParentId>& aOptionalContentParentId, const nsACString& aGroup, const nsACString& aOrigin, uint32_t aTelemetryId, FullDatabaseMetadata* aMetadata, FileManager* aFileManager, already_AddRefed<DirectoryLock> aDirectoryLock, bool aFileHandleDisabled, bool aChromeWriteAccessAllowed) : mFactory(aFactory) , mMetadata(aMetadata) , mFileManager(aFileManager) , mDirectoryLock(Move(aDirectoryLock)) , mPrincipalInfo(aPrincipalInfo) , mOptionalContentParentId(aOptionalContentParentId) , mGroup(aGroup) , mOrigin(aOrigin) , mId(aMetadata->mDatabaseId) , mFilePath(aMetadata->mFilePath) , mActiveMutableFileCount(0) , mTelemetryId(aTelemetryId) , mPersistenceType(aMetadata->mCommonMetadata.persistenceType()) , mFileHandleDisabled(aFileHandleDisabled) , mChromeWriteAccessAllowed(aChromeWriteAccessAllowed) , mClosed(false) , mInvalidated(false) , mActorWasAlive(false) , mActorDestroyed(false) , mMetadataCleanedUp(false) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aFactory); MOZ_ASSERT(aMetadata); MOZ_ASSERT(aFileManager); MOZ_ASSERT_IF(aChromeWriteAccessAllowed, aPrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo); } void Database::Invalidate() { AssertIsOnBackgroundThread(); class MOZ_STACK_CLASS Helper final { public: static bool InvalidateTransactions(nsTHashtable<nsPtrHashKey<TransactionBase>>& aTable) { AssertIsOnBackgroundThread(); const uint32_t count = aTable.Count(); if (!count) { return true; } FallibleTArray<RefPtr<TransactionBase>> transactions; if (NS_WARN_IF(!transactions.SetCapacity(count, fallible))) { return false; } for (auto iter = aTable.Iter(); !iter.Done(); iter.Next()) { if (NS_WARN_IF(!transactions.AppendElement(iter.Get()->GetKey(), fallible))) { return false; } } if (count) { IDB_REPORT_INTERNAL_ERR(); for (uint32_t index = 0; index < count; index++) { RefPtr<TransactionBase> transaction = transactions[index].forget(); MOZ_ASSERT(transaction); transaction->Invalidate(); } } return true; } static bool InvalidateMutableFiles(nsTHashtable<nsPtrHashKey<MutableFile>>& aTable) { AssertIsOnBackgroundThread(); const uint32_t count = aTable.Count(); if (!count) { return true; } FallibleTArray<RefPtr<MutableFile>> mutableFiles; if (NS_WARN_IF(!mutableFiles.SetCapacity(count, fallible))) { return false; } for (auto iter = aTable.Iter(); !iter.Done(); iter.Next()) { if (NS_WARN_IF(!mutableFiles.AppendElement(iter.Get()->GetKey(), fallible))) { return false; } } if (count) { IDB_REPORT_INTERNAL_ERR(); for (uint32_t index = 0; index < count; index++) { RefPtr<MutableFile> mutableFile = mutableFiles[index].forget(); MOZ_ASSERT(mutableFile); mutableFile->Invalidate(); } } return true; } }; if (mInvalidated) { return; } mInvalidated = true; if (mActorWasAlive && !mActorDestroyed) { Unused << SendInvalidate(); } if (!Helper::InvalidateTransactions(mTransactions)) { NS_WARNING("Failed to abort all transactions!"); } if (!Helper::InvalidateMutableFiles(mMutableFiles)) { NS_WARNING("Failed to abort all mutable files!"); } MOZ_ALWAYS_TRUE(CloseInternal()); CleanupMetadata(); } nsresult Database::EnsureConnection() { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!IsOnBackgroundThread()); PROFILER_LABEL("IndexedDB", "Database::EnsureConnection", js::ProfileEntry::Category::STORAGE); if (!mConnection || !mConnection->GetStorageConnection()) { nsresult rv = gConnectionPool->GetOrCreateConnection(this, getter_AddRefs(mConnection)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } AssertIsOnConnectionThread(); return NS_OK; } bool Database::RegisterTransaction(TransactionBase* aTransaction) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aTransaction); MOZ_ASSERT(!mTransactions.GetEntry(aTransaction)); MOZ_ASSERT(mDirectoryLock); MOZ_ASSERT(!mInvalidated); MOZ_ASSERT(!mClosed); if (NS_WARN_IF(!mTransactions.PutEntry(aTransaction, fallible))) { return false; } return true; } void Database::UnregisterTransaction(TransactionBase* aTransaction) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aTransaction); MOZ_ASSERT(mTransactions.GetEntry(aTransaction)); mTransactions.RemoveEntry(aTransaction); MaybeCloseConnection(); } bool Database::RegisterMutableFile(MutableFile* aMutableFile) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aMutableFile); MOZ_ASSERT(!mMutableFiles.GetEntry(aMutableFile)); MOZ_ASSERT(mDirectoryLock); if (NS_WARN_IF(!mMutableFiles.PutEntry(aMutableFile, fallible))) { return false; } return true; } void Database::UnregisterMutableFile(MutableFile* aMutableFile) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aMutableFile); MOZ_ASSERT(mMutableFiles.GetEntry(aMutableFile)); mMutableFiles.RemoveEntry(aMutableFile); } void Database::NoteActiveMutableFile() { AssertIsOnBackgroundThread(); MOZ_ASSERT(mDirectoryLock); MOZ_ASSERT(mActiveMutableFileCount < UINT32_MAX); ++mActiveMutableFileCount; } void Database::NoteInactiveMutableFile() { AssertIsOnBackgroundThread(); MOZ_ASSERT(mActiveMutableFileCount > 0); --mActiveMutableFileCount; MaybeCloseConnection(); } void Database::SetActorAlive() { AssertIsOnBackgroundThread(); MOZ_ASSERT(!mActorWasAlive); MOZ_ASSERT(!mActorDestroyed); mActorWasAlive = true; // This reference will be absorbed by IPDL and released when the actor is // destroyed. AddRef(); } bool Database::CloseInternal() { AssertIsOnBackgroundThread(); if (mClosed) { if (NS_WARN_IF(!IsInvalidated())) { // Kill misbehaving child for sending the close message twice. return false; } // Ignore harmless race when we just invalidated the database. return true; } mClosed = true; 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() && !mActiveMutableFileCount && IsClosed() && mDirectoryLock) { nsCOMPtr<nsIRunnable> callback = NewRunnableMethod(this, &Database::ConnectionClosedCallback); RefPtr<WaitForTransactionsHelper> helper = new WaitForTransactionsHelper(Id(), callback); helper->WaitForTransactions(); } } void Database::ConnectionClosedCallback() { AssertIsOnBackgroundThread(); MOZ_ASSERT(mClosed); MOZ_ASSERT(!mTransactions.Count()); MOZ_ASSERT(!mActiveMutableFileCount); mDirectoryLock = nullptr; CleanupMetadata(); 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(); if (!mMetadataCleanedUp) { mMetadataCleanedUp = true; DatabaseActorInfo* info; MOZ_ALWAYS_TRUE(gLiveDatabaseHashtable->Get(Id(), &info)); MOZ_ALWAYS_TRUE(info->mLiveDatabases.RemoveElement(this)); if (info->mLiveDatabases.IsEmpty()) { MOZ_ASSERT(!info->mWaitingFactoryOp || !info->mWaitingFactoryOp->HasBlockedDatabases()); gLiveDatabaseHashtable->Remove(Id()); } // Match the IncreaseBusyCount in OpenDatabaseOp::EnsureDatabaseActor(). DecreaseBusyCount(); } } bool Database::VerifyRequestParams(const DatabaseRequestParams& aParams) const { AssertIsOnBackgroundThread(); MOZ_ASSERT(aParams.type() != DatabaseRequestParams::T__None); switch (aParams.type()) { case DatabaseRequestParams::TCreateFileParams: { if (NS_WARN_IF(mFileHandleDisabled)) { ASSERT_UNLESS_FUZZING(); return false; } const CreateFileParams& params = aParams.get_CreateFileParams(); if (NS_WARN_IF(params.name().IsEmpty())) { ASSERT_UNLESS_FUZZING(); return false; } break; } default: MOZ_CRASH("Should never get here!"); } return true; } void Database::ActorDestroy(ActorDestroyReason aWhy) { AssertIsOnBackgroundThread(); MOZ_ASSERT(!mActorDestroyed); mActorDestroyed = true; if (!IsInvalidated()) { Invalidate(); } } PBackgroundIDBDatabaseFileParent* Database::AllocPBackgroundIDBDatabaseFileParent(PBlobParent* aBlobParent) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aBlobParent); RefPtr<BlobImpl> blobImpl = static_cast<BlobParent*>(aBlobParent)->GetBlobImpl(); MOZ_ASSERT(blobImpl); RefPtr<FileInfo> fileInfo; RefPtr<DatabaseFile> actor; RefPtr<BlobImplStoredFile> storedFileImpl = do_QueryObject(blobImpl); if (storedFileImpl && storedFileImpl->IsShareable(mFileManager)) { // This blob was previously shared with the child. fileInfo = storedFileImpl->GetFileInfo(); MOZ_ASSERT(fileInfo); actor = new DatabaseFile(fileInfo); } else { // This is a blob we haven't seen before. fileInfo = mFileManager->GetNewFileInfo(); MOZ_ASSERT(fileInfo); actor = new DatabaseFile(blobImpl, fileInfo); } MOZ_ASSERT(actor); return actor.forget().take(); } bool Database::DeallocPBackgroundIDBDatabaseFileParent( PBackgroundIDBDatabaseFileParent* aActor) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); RefPtr<DatabaseFile> actor = dont_AddRef(static_cast<DatabaseFile*>(aActor)); return true; } PBackgroundIDBDatabaseRequestParent* Database::AllocPBackgroundIDBDatabaseRequestParent( const DatabaseRequestParams& aParams) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aParams.type() != DatabaseRequestParams::T__None); #ifdef DEBUG // Always verify parameters in DEBUG builds! bool trustParams = false; #else PBackgroundParent* backgroundActor = GetBackgroundParent(); MOZ_ASSERT(backgroundActor); bool trustParams = !BackgroundParent::IsOtherProcessActor(backgroundActor); #endif if (NS_WARN_IF(!trustParams && !VerifyRequestParams(aParams))) { ASSERT_UNLESS_FUZZING(); return nullptr; } RefPtr<DatabaseOp> actor; switch (aParams.type()) { case DatabaseRequestParams::TCreateFileParams: { actor = new CreateFileOp(this, aParams); break; } default: MOZ_CRASH("Should never get here!"); } MOZ_ASSERT(actor); // Transfer ownership to IPDL. return actor.forget().take(); } bool Database::RecvPBackgroundIDBDatabaseRequestConstructor( PBackgroundIDBDatabaseRequestParent* aActor, const DatabaseRequestParams& aParams) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); MOZ_ASSERT(aParams.type() != DatabaseRequestParams::T__None); auto* op = static_cast<DatabaseOp*>(aActor); op->RunImmediately(); return true; } bool Database::DeallocPBackgroundIDBDatabaseRequestParent( PBackgroundIDBDatabaseRequestParent* aActor) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); // Transfer ownership back from IPDL. RefPtr<DatabaseOp> op = dont_AddRef(static_cast<DatabaseOp*>(aActor)); return true; } PBackgroundIDBTransactionParent* Database::AllocPBackgroundIDBTransactionParent( const nsTArray<nsString>& aObjectStoreNames, const Mode& aMode) { AssertIsOnBackgroundThread(); // Once a database is closed it must not try to open new transactions. if (NS_WARN_IF(mClosed)) { if (!mInvalidated) { ASSERT_UNLESS_FUZZING(); } return nullptr; } if (NS_WARN_IF(aObjectStoreNames.IsEmpty())) { ASSERT_UNLESS_FUZZING(); return nullptr; } if (NS_WARN_IF(aMode != IDBTransaction::READ_ONLY && aMode != IDBTransaction::READ_WRITE && aMode != IDBTransaction::READ_WRITE_FLUSH && aMode != IDBTransaction::CLEANUP)) { ASSERT_UNLESS_FUZZING(); return nullptr; } // If this is a readwrite transaction to a chrome database make sure the child // has write access. if (NS_WARN_IF((aMode == IDBTransaction::READ_WRITE || aMode == IDBTransaction::READ_WRITE_FLUSH || aMode == IDBTransaction::CLEANUP) && mPrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo && !mChromeWriteAccessAllowed)) { return nullptr; } const ObjectStoreTable& objectStores = mMetadata->mObjectStores; const uint32_t nameCount = aObjectStoreNames.Length(); if (NS_WARN_IF(nameCount > objectStores.Count())) { ASSERT_UNLESS_FUZZING(); return nullptr; } FallibleTArray<RefPtr<FullObjectStoreMetadata>> fallibleObjectStores; if (NS_WARN_IF(!fallibleObjectStores.SetCapacity(nameCount, fallible))) { return nullptr; } for (uint32_t nameIndex = 0; nameIndex < nameCount; nameIndex++) { const nsString& name = aObjectStoreNames[nameIndex]; if (nameIndex) { // Make sure that this name is sorted properly and not a duplicate. if (NS_WARN_IF(name <= aObjectStoreNames[nameIndex - 1])) { ASSERT_UNLESS_FUZZING(); return nullptr; } } for (auto iter = objectStores.ConstIter(); !iter.Done(); iter.Next()) { auto value = iter.Data(); MOZ_ASSERT(iter.Key()); if (name == value->mCommonMetadata.name() && !value->mDeleted) { if (NS_WARN_IF(!fallibleObjectStores.AppendElement(value, fallible))) { return nullptr; } break; } } } nsTArray<RefPtr<FullObjectStoreMetadata>> infallibleObjectStores; infallibleObjectStores.SwapElements(fallibleObjectStores); RefPtr<NormalTransaction> transaction = new NormalTransaction(this, aMode, infallibleObjectStores); MOZ_ASSERT(infallibleObjectStores.IsEmpty()); return transaction.forget().take(); } bool Database::RecvPBackgroundIDBTransactionConstructor( PBackgroundIDBTransactionParent* aActor, InfallibleTArray<nsString>&& aObjectStoreNames, const Mode& aMode) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); MOZ_ASSERT(!aObjectStoreNames.IsEmpty()); MOZ_ASSERT(aMode == IDBTransaction::READ_ONLY || aMode == IDBTransaction::READ_WRITE || aMode == IDBTransaction::READ_WRITE_FLUSH || aMode == IDBTransaction::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 true; } if (!gConnectionPool) { gConnectionPool = new ConnectionPool(); } auto* transaction = static_cast<NormalTransaction*>(aActor); RefPtr<StartTransactionOp> startOp = new StartTransactionOp(transaction); uint64_t transactionId = startOp->StartOnConnectionPool(GetLoggingInfo()->Id(), mMetadata->mDatabaseId, transaction->LoggingSerialNumber(), aObjectStoreNames, aMode != IDBTransaction::READ_ONLY); transaction->SetActive(transactionId); if (NS_WARN_IF(!RegisterTransaction(transaction))) { IDB_REPORT_INTERNAL_ERR(); transaction->Abort(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR, /* aForce */ false); return true; } return true; } bool Database::DeallocPBackgroundIDBTransactionParent( PBackgroundIDBTransactionParent* aActor) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); RefPtr<NormalTransaction> transaction = dont_AddRef(static_cast<NormalTransaction*>(aActor)); return true; } PBackgroundIDBVersionChangeTransactionParent* Database::AllocPBackgroundIDBVersionChangeTransactionParent( const uint64_t& aCurrentVersion, const uint64_t& aRequestedVersion, const int64_t& aNextObjectStoreId, const int64_t& aNextIndexId) { MOZ_CRASH("PBackgroundIDBVersionChangeTransactionParent actors should be " "constructed manually!"); } bool Database::DeallocPBackgroundIDBVersionChangeTransactionParent( PBackgroundIDBVersionChangeTransactionParent* aActor) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); RefPtr<VersionChangeTransaction> transaction = dont_AddRef(static_cast<VersionChangeTransaction*>(aActor)); return true; } Database::PBackgroundMutableFileParent* Database::AllocPBackgroundMutableFileParent(const nsString& aName, const nsString& aType) { MOZ_CRASH("PBackgroundMutableFileParent actors should be constructed " "manually!"); } bool Database::DeallocPBackgroundMutableFileParent( PBackgroundMutableFileParent* aActor) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); // Transfer ownership back from IPDL. RefPtr<MutableFile> mutableFile = dont_AddRef(static_cast<MutableFile*>(aActor)); return true; } bool Database::RecvDeleteMe() { AssertIsOnBackgroundThread(); MOZ_ASSERT(!mActorDestroyed); return PBackgroundIDBDatabaseParent::Send__delete__(this); } bool Database::RecvBlocked() { AssertIsOnBackgroundThread(); if (NS_WARN_IF(mClosed)) { return false; } DatabaseActorInfo* info; MOZ_ALWAYS_TRUE(gLiveDatabaseHashtable->Get(Id(), &info)); MOZ_ASSERT(info->mLiveDatabases.Contains(this)); MOZ_ASSERT(info->mWaitingFactoryOp); info->mWaitingFactoryOp->NoteDatabaseBlocked(this); return true; } bool Database::RecvClose() { AssertIsOnBackgroundThread(); if (NS_WARN_IF(!CloseInternal())) { ASSERT_UNLESS_FUZZING(); return false; } return true; } void Database:: StartTransactionOp::RunOnConnectionThread() { MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(Transaction()); MOZ_ASSERT(NS_SUCCEEDED(mResultCode)); IDB_LOG_MARK("IndexedDB %s: Parent Transaction[%lld]: " "Beginning database work", "IndexedDB %s: P T[%lld]: DB Start", IDB_LOG_ID_STRING(mBackgroundChildLoggingId), mLoggingSerialNumber); TransactionDatabaseOperationBase::RunOnConnectionThread(); } nsresult Database:: StartTransactionOp::DoDatabaseWork(DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); Transaction()->SetActiveOnConnectionThread(); if (Transaction()->GetMode() == IDBTransaction::CLEANUP) { nsresult rv = aConnection->DisableQuotaChecks(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } if (Transaction()->GetMode() != IDBTransaction::READ_ONLY) { nsresult rv = aConnection->BeginWriteTransaction(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } 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(Database* aDatabase, Mode aMode) : mDatabase(aDatabase) , mTransactionId(0) , mDatabaseId(aDatabase->Id()) , mLoggingSerialNumber(aDatabase->GetLoggingInfo()->NextTransactionSN(aMode)) , mActiveRequestCount(0) , mInvalidatedOnAnyThread(false) , mMode(aMode) , mHasBeenActive(false) , mHasBeenActiveOnConnectionThread(false) , mActorDestroyed(false) , mInvalidated(false) , mResultCode(NS_OK) , mCommitOrAbortReceived(false) , mCommittedOrAborted(false) , mForceAborted(false) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aDatabase); MOZ_ASSERT(mLoggingSerialNumber); } TransactionBase::~TransactionBase() { MOZ_ASSERT(!mActiveRequestCount); MOZ_ASSERT(mActorDestroyed); MOZ_ASSERT_IF(mHasBeenActive, mCommittedOrAborted); } void TransactionBase::Abort(nsresult aResultCode, bool aForce) { AssertIsOnBackgroundThread(); MOZ_ASSERT(NS_FAILED(aResultCode)); if (NS_SUCCEEDED(mResultCode)) { mResultCode = aResultCode; } if (aForce) { mForceAborted = true; } MaybeCommitOrAbort(); } bool TransactionBase::RecvCommit() { AssertIsOnBackgroundThread(); if (NS_WARN_IF(mCommitOrAbortReceived)) { ASSERT_UNLESS_FUZZING(); return false; } mCommitOrAbortReceived = true; MaybeCommitOrAbort(); return true; } bool TransactionBase::RecvAbort(nsresult aResultCode) { AssertIsOnBackgroundThread(); if (NS_WARN_IF(NS_SUCCEEDED(aResultCode))) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(NS_ERROR_GET_MODULE(aResultCode) != NS_ERROR_MODULE_DOM_INDEXEDDB)) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(mCommitOrAbortReceived)) { ASSERT_UNLESS_FUZZING(); return false; } mCommitOrAbortReceived = true; Abort(aResultCode, /* aForce */ false); return true; } void TransactionBase::CommitOrAbort() { AssertIsOnBackgroundThread(); MOZ_ASSERT(!mCommittedOrAborted); mCommittedOrAborted = true; if (!mHasBeenActive) { return; } RefPtr<CommitOp> commitOp = new CommitOp(this, ClampResultCode(mResultCode)); gConnectionPool->Finish(TransactionId(), commitOp); } already_AddRefed<FullObjectStoreMetadata> TransactionBase::GetMetadataForObjectStoreId(int64_t aObjectStoreId) const { AssertIsOnBackgroundThread(); MOZ_ASSERT(aObjectStoreId); if (!aObjectStoreId) { return nullptr; } RefPtr<FullObjectStoreMetadata> metadata; if (!mDatabase->Metadata()->mObjectStores.Get(aObjectStoreId, getter_AddRefs(metadata)) || metadata->mDeleted) { return nullptr; } MOZ_ASSERT(metadata->mCommonMetadata.id() == aObjectStoreId); return metadata.forget(); } already_AddRefed<FullIndexMetadata> TransactionBase::GetMetadataForIndexId( FullObjectStoreMetadata* const aObjectStoreMetadata, int64_t aIndexId) const { AssertIsOnBackgroundThread(); MOZ_ASSERT(aIndexId); if (!aIndexId) { return nullptr; } RefPtr<FullIndexMetadata> metadata; if (!aObjectStoreMetadata->mIndexes.Get(aIndexId, getter_AddRefs(metadata)) || metadata->mDeleted) { return nullptr; } MOZ_ASSERT(metadata->mCommonMetadata.id() == aIndexId); return metadata.forget(); } void TransactionBase::NoteModifiedAutoIncrementObjectStore( FullObjectStoreMetadata* aMetadata) { AssertIsOnConnectionThread(); MOZ_ASSERT(aMetadata); if (!mModifiedAutoIncrementObjectStoreMetadataArray.Contains(aMetadata)) { mModifiedAutoIncrementObjectStoreMetadataArray.AppendElement(aMetadata); } } void TransactionBase::ForgetModifiedAutoIncrementObjectStore( FullObjectStoreMetadata* aMetadata) { AssertIsOnConnectionThread(); MOZ_ASSERT(aMetadata); 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_WARN_IF(!VerifyRequestParams(params))) { ASSERT_UNLESS_FUZZING(); return false; } break; } case RequestParams::TObjectStorePutParams: { const ObjectStoreAddPutParams& params = aParams.get_ObjectStorePutParams().commonParams(); if (NS_WARN_IF(!VerifyRequestParams(params))) { ASSERT_UNLESS_FUZZING(); return false; } break; } case RequestParams::TObjectStoreGetParams: { const ObjectStoreGetParams& params = aParams.get_ObjectStoreGetParams(); const RefPtr<FullObjectStoreMetadata> objectStoreMetadata = GetMetadataForObjectStoreId(params.objectStoreId()); if (NS_WARN_IF(!objectStoreMetadata)) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(!VerifyRequestParams(params.keyRange()))) { ASSERT_UNLESS_FUZZING(); return false; } break; } case RequestParams::TObjectStoreGetKeyParams: { const ObjectStoreGetKeyParams& params = aParams.get_ObjectStoreGetKeyParams(); const RefPtr<FullObjectStoreMetadata> objectStoreMetadata = GetMetadataForObjectStoreId(params.objectStoreId()); if (NS_WARN_IF(!objectStoreMetadata)) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(!VerifyRequestParams(params.keyRange()))) { ASSERT_UNLESS_FUZZING(); return false; } break; } case RequestParams::TObjectStoreGetAllParams: { const ObjectStoreGetAllParams& params = aParams.get_ObjectStoreGetAllParams(); const RefPtr<FullObjectStoreMetadata> objectStoreMetadata = GetMetadataForObjectStoreId(params.objectStoreId()); if (NS_WARN_IF(!objectStoreMetadata)) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(!VerifyRequestParams(params.optionalKeyRange()))) { ASSERT_UNLESS_FUZZING(); return false; } break; } case RequestParams::TObjectStoreGetAllKeysParams: { const ObjectStoreGetAllKeysParams& params = aParams.get_ObjectStoreGetAllKeysParams(); const RefPtr<FullObjectStoreMetadata> objectStoreMetadata = GetMetadataForObjectStoreId(params.objectStoreId()); if (NS_WARN_IF(!objectStoreMetadata)) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(!VerifyRequestParams(params.optionalKeyRange()))) { ASSERT_UNLESS_FUZZING(); return false; } break; } case RequestParams::TObjectStoreDeleteParams: { if (NS_WARN_IF(mMode != IDBTransaction::READ_WRITE && mMode != IDBTransaction::READ_WRITE_FLUSH && mMode != IDBTransaction::CLEANUP && mMode != IDBTransaction::VERSION_CHANGE)) { ASSERT_UNLESS_FUZZING(); return false; } const ObjectStoreDeleteParams& params = aParams.get_ObjectStoreDeleteParams(); const RefPtr<FullObjectStoreMetadata> objectStoreMetadata = GetMetadataForObjectStoreId(params.objectStoreId()); if (NS_WARN_IF(!objectStoreMetadata)) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(!VerifyRequestParams(params.keyRange()))) { ASSERT_UNLESS_FUZZING(); return false; } break; } case RequestParams::TObjectStoreClearParams: { if (NS_WARN_IF(mMode != IDBTransaction::READ_WRITE && mMode != IDBTransaction::READ_WRITE_FLUSH && mMode != IDBTransaction::CLEANUP && mMode != IDBTransaction::VERSION_CHANGE)) { ASSERT_UNLESS_FUZZING(); return false; } const ObjectStoreClearParams& params = aParams.get_ObjectStoreClearParams(); const RefPtr<FullObjectStoreMetadata> objectStoreMetadata = GetMetadataForObjectStoreId(params.objectStoreId()); if (NS_WARN_IF(!objectStoreMetadata)) { ASSERT_UNLESS_FUZZING(); return false; } break; } case RequestParams::TObjectStoreCountParams: { const ObjectStoreCountParams& params = aParams.get_ObjectStoreCountParams(); const RefPtr<FullObjectStoreMetadata> objectStoreMetadata = GetMetadataForObjectStoreId(params.objectStoreId()); if (NS_WARN_IF(!objectStoreMetadata)) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(!VerifyRequestParams(params.optionalKeyRange()))) { ASSERT_UNLESS_FUZZING(); return false; } break; } case RequestParams::TIndexGetParams: { const IndexGetParams& params = aParams.get_IndexGetParams(); const RefPtr<FullObjectStoreMetadata> objectStoreMetadata = GetMetadataForObjectStoreId(params.objectStoreId()); if (NS_WARN_IF(!objectStoreMetadata)) { ASSERT_UNLESS_FUZZING(); return false; } const RefPtr<FullIndexMetadata> indexMetadata = GetMetadataForIndexId(objectStoreMetadata, params.indexId()); if (NS_WARN_IF(!indexMetadata)) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(!VerifyRequestParams(params.keyRange()))) { ASSERT_UNLESS_FUZZING(); return false; } break; } case RequestParams::TIndexGetKeyParams: { const IndexGetKeyParams& params = aParams.get_IndexGetKeyParams(); const RefPtr<FullObjectStoreMetadata> objectStoreMetadata = GetMetadataForObjectStoreId(params.objectStoreId()); if (NS_WARN_IF(!objectStoreMetadata)) { ASSERT_UNLESS_FUZZING(); return false; } const RefPtr<FullIndexMetadata> indexMetadata = GetMetadataForIndexId(objectStoreMetadata, params.indexId()); if (NS_WARN_IF(!indexMetadata)) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(!VerifyRequestParams(params.keyRange()))) { ASSERT_UNLESS_FUZZING(); return false; } break; } case RequestParams::TIndexGetAllParams: { const IndexGetAllParams& params = aParams.get_IndexGetAllParams(); const RefPtr<FullObjectStoreMetadata> objectStoreMetadata = GetMetadataForObjectStoreId(params.objectStoreId()); if (NS_WARN_IF(!objectStoreMetadata)) { ASSERT_UNLESS_FUZZING(); return false; } const RefPtr<FullIndexMetadata> indexMetadata = GetMetadataForIndexId(objectStoreMetadata, params.indexId()); if (NS_WARN_IF(!indexMetadata)) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(!VerifyRequestParams(params.optionalKeyRange()))) { ASSERT_UNLESS_FUZZING(); return false; } break; } case RequestParams::TIndexGetAllKeysParams: { const IndexGetAllKeysParams& params = aParams.get_IndexGetAllKeysParams(); const RefPtr<FullObjectStoreMetadata> objectStoreMetadata = GetMetadataForObjectStoreId(params.objectStoreId()); if (NS_WARN_IF(!objectStoreMetadata)) { ASSERT_UNLESS_FUZZING(); return false; } const RefPtr<FullIndexMetadata> indexMetadata = GetMetadataForIndexId(objectStoreMetadata, params.indexId()); if (NS_WARN_IF(!indexMetadata)) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(!VerifyRequestParams(params.optionalKeyRange()))) { ASSERT_UNLESS_FUZZING(); return false; } break; } case RequestParams::TIndexCountParams: { const IndexCountParams& params = aParams.get_IndexCountParams(); const RefPtr<FullObjectStoreMetadata> objectStoreMetadata = GetMetadataForObjectStoreId(params.objectStoreId()); if (NS_WARN_IF(!objectStoreMetadata)) { ASSERT_UNLESS_FUZZING(); return false; } const RefPtr<FullIndexMetadata> indexMetadata = GetMetadataForIndexId(objectStoreMetadata, params.indexId()); if (NS_WARN_IF(!indexMetadata)) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(!VerifyRequestParams(params.optionalKeyRange()))) { ASSERT_UNLESS_FUZZING(); 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_WARN_IF(aParams.lower().IsUnset())) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(!aParams.upper().IsUnset())) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(aParams.lowerOpen())) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(aParams.upperOpen())) { ASSERT_UNLESS_FUZZING(); return false; } } else if (NS_WARN_IF(aParams.lower().IsUnset() && aParams.upper().IsUnset())) { ASSERT_UNLESS_FUZZING(); return false; } return true; } bool TransactionBase::VerifyRequestParams(const ObjectStoreAddPutParams& aParams) const { AssertIsOnBackgroundThread(); if (NS_WARN_IF(mMode != IDBTransaction::READ_WRITE && mMode != IDBTransaction::READ_WRITE_FLUSH && mMode != IDBTransaction::VERSION_CHANGE)) { ASSERT_UNLESS_FUZZING(); return false; } RefPtr<FullObjectStoreMetadata> objMetadata = GetMetadataForObjectStoreId(aParams.objectStoreId()); if (NS_WARN_IF(!objMetadata)) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(!aParams.cloneInfo().data().data.Size())) { ASSERT_UNLESS_FUZZING(); return false; } if (objMetadata->mCommonMetadata.autoIncrement() && objMetadata->mCommonMetadata.keyPath().IsValid() && aParams.key().IsUnset()) { const SerializedStructuredCloneWriteInfo cloneInfo = aParams.cloneInfo(); if (NS_WARN_IF(!cloneInfo.offsetToKeyProp())) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(cloneInfo.data().data.Size() < sizeof(uint64_t))) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(cloneInfo.offsetToKeyProp() > (cloneInfo.data().data.Size() - sizeof(uint64_t)))) { ASSERT_UNLESS_FUZZING(); return false; } } else if (NS_WARN_IF(aParams.cloneInfo().offsetToKeyProp())) { ASSERT_UNLESS_FUZZING(); return false; } const nsTArray<IndexUpdateInfo>& updates = aParams.indexUpdateInfos(); for (uint32_t index = 0; index < updates.Length(); index++) { RefPtr<FullIndexMetadata> indexMetadata = GetMetadataForIndexId(objMetadata, updates[index].indexId()); if (NS_WARN_IF(!indexMetadata)) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(updates[index].value().IsUnset())) { ASSERT_UNLESS_FUZZING(); return false; } MOZ_ASSERT(!updates[index].value().GetBuffer().IsEmpty()); } const nsTArray<FileAddInfo>& fileAddInfos = aParams.fileAddInfos(); for (uint32_t index = 0; index < fileAddInfos.Length(); index++) { const FileAddInfo& fileAddInfo = fileAddInfos[index]; const DatabaseOrMutableFile& file = fileAddInfo.file(); MOZ_ASSERT(file.type() != DatabaseOrMutableFile::T__None); switch (fileAddInfo.type()) { case StructuredCloneFile::eBlob: if (NS_WARN_IF(file.type() != DatabaseOrMutableFile::TPBackgroundIDBDatabaseFileParent)) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(!file.get_PBackgroundIDBDatabaseFileParent())) { ASSERT_UNLESS_FUZZING(); return false; } break; case StructuredCloneFile::eMutableFile: { if (NS_WARN_IF(file.type() != DatabaseOrMutableFile::TPBackgroundMutableFileParent)) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(mDatabase->IsFileHandleDisabled())) { ASSERT_UNLESS_FUZZING(); return false; } auto mutableFile = static_cast<MutableFile*>(file.get_PBackgroundMutableFileParent()); if (NS_WARN_IF(!mutableFile)) { ASSERT_UNLESS_FUZZING(); return false; } Database* database = mutableFile->GetDatabase(); if (NS_WARN_IF(!database)) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(database->Id() != mDatabase->Id())) { ASSERT_UNLESS_FUZZING(); return false; } break; } case StructuredCloneFile::eStructuredClone: ASSERT_UNLESS_FUZZING(); return false; case StructuredCloneFile::eWasmBytecode: case StructuredCloneFile::eWasmCompiled: if (NS_WARN_IF(file.type() != DatabaseOrMutableFile::TPBackgroundIDBDatabaseFileParent)) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(!file.get_PBackgroundIDBDatabaseFileParent())) { ASSERT_UNLESS_FUZZING(); return false; } break; case StructuredCloneFile::eEndGuard: ASSERT_UNLESS_FUZZING(); return false; default: MOZ_CRASH("Should never get here!"); } } return true; } bool TransactionBase::VerifyRequestParams(const OptionalKeyRange& aParams) const { AssertIsOnBackgroundThread(); MOZ_ASSERT(aParams.type() != OptionalKeyRange::T__None); switch (aParams.type()) { case OptionalKeyRange::TSerializedKeyRange: if (NS_WARN_IF(!VerifyRequestParams(aParams.get_SerializedKeyRange()))) { ASSERT_UNLESS_FUZZING(); return false; } break; case OptionalKeyRange::Tvoid_t: break; default: MOZ_CRASH("Should never get here!"); } return true; } void TransactionBase::NoteActiveRequest() { AssertIsOnBackgroundThread(); MOZ_ASSERT(mActiveRequestCount < UINT64_MAX); mActiveRequestCount++; } void TransactionBase::NoteFinishedRequest() { AssertIsOnBackgroundThread(); MOZ_ASSERT(mActiveRequestCount); mActiveRequestCount--; MaybeCommitOrAbort(); } void TransactionBase::Invalidate() { AssertIsOnBackgroundThread(); MOZ_ASSERT(mInvalidated == mInvalidatedOnAnyThread); if (!mInvalidated) { mInvalidated = true; mInvalidatedOnAnyThread = true; Abort(NS_ERROR_DOM_INDEXEDDB_ABORT_ERR, /* aForce */ false); } } PBackgroundIDBRequestParent* TransactionBase::AllocRequest(const RequestParams& aParams, bool aTrustParams) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aParams.type() != RequestParams::T__None); #ifdef DEBUG // Always verify parameters in DEBUG builds! aTrustParams = false; #endif if (!aTrustParams && NS_WARN_IF(!VerifyRequestParams(aParams))) { ASSERT_UNLESS_FUZZING(); return nullptr; } if (NS_WARN_IF(mCommitOrAbortReceived)) { ASSERT_UNLESS_FUZZING(); return nullptr; } RefPtr<NormalTransactionOp> actor; switch (aParams.type()) { case RequestParams::TObjectStoreAddParams: case RequestParams::TObjectStorePutParams: actor = new ObjectStoreAddOrPutRequestOp(this, aParams); break; case RequestParams::TObjectStoreGetParams: actor = new ObjectStoreGetRequestOp(this, aParams, /* aGetAll */ false); break; case RequestParams::TObjectStoreGetAllParams: actor = new ObjectStoreGetRequestOp(this, aParams, /* aGetAll */ true); break; case RequestParams::TObjectStoreGetKeyParams: actor = new ObjectStoreGetKeyRequestOp(this, aParams, /* aGetAll */ false); break; case RequestParams::TObjectStoreGetAllKeysParams: actor = new ObjectStoreGetKeyRequestOp(this, aParams, /* aGetAll */ true); break; case RequestParams::TObjectStoreDeleteParams: actor = new ObjectStoreDeleteRequestOp(this, aParams.get_ObjectStoreDeleteParams()); break; case RequestParams::TObjectStoreClearParams: actor = new ObjectStoreClearRequestOp(this, aParams.get_ObjectStoreClearParams()); break; case RequestParams::TObjectStoreCountParams: actor = new ObjectStoreCountRequestOp(this, aParams.get_ObjectStoreCountParams()); break; case RequestParams::TIndexGetParams: actor = new IndexGetRequestOp(this, aParams, /* aGetAll */ false); break; case RequestParams::TIndexGetKeyParams: actor = new IndexGetKeyRequestOp(this, aParams, /* aGetAll */ false); break; case RequestParams::TIndexGetAllParams: actor = new IndexGetRequestOp(this, aParams, /* aGetAll */ true); break; case RequestParams::TIndexGetAllKeysParams: actor = new IndexGetKeyRequestOp(this, aParams, /* aGetAll */ true); break; case RequestParams::TIndexCountParams: actor = new IndexCountRequestOp(this, 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<NormalTransactionOp*>(aActor); if (NS_WARN_IF(!op->Init(this))) { op->Cleanup(); return false; } op->DispatchToConnectionPool(); return true; } bool TransactionBase::DeallocRequest(PBackgroundIDBRequestParent* aActor) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); // Transfer ownership back from IPDL. RefPtr<NormalTransactionOp> actor = dont_AddRef(static_cast<NormalTransactionOp*>(aActor)); return true; } PBackgroundIDBCursorParent* 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 OpenCursorParams::Type type = aParams.type(); RefPtr<FullObjectStoreMetadata> objectStoreMetadata; RefPtr<FullIndexMetadata> indexMetadata; Cursor::Direction direction; switch (type) { case OpenCursorParams::TObjectStoreOpenCursorParams: { const ObjectStoreOpenCursorParams& params = aParams.get_ObjectStoreOpenCursorParams(); objectStoreMetadata = GetMetadataForObjectStoreId(params.objectStoreId()); if (NS_WARN_IF(!objectStoreMetadata)) { ASSERT_UNLESS_FUZZING(); return nullptr; } if (aTrustParams && NS_WARN_IF(!VerifyRequestParams(params.optionalKeyRange()))) { ASSERT_UNLESS_FUZZING(); return nullptr; } direction = params.direction(); break; } case OpenCursorParams::TObjectStoreOpenKeyCursorParams: { const ObjectStoreOpenKeyCursorParams& params = aParams.get_ObjectStoreOpenKeyCursorParams(); objectStoreMetadata = GetMetadataForObjectStoreId(params.objectStoreId()); if (NS_WARN_IF(!objectStoreMetadata)) { ASSERT_UNLESS_FUZZING(); return nullptr; } if (aTrustParams && NS_WARN_IF(!VerifyRequestParams(params.optionalKeyRange()))) { ASSERT_UNLESS_FUZZING(); return nullptr; } direction = params.direction(); break; } case OpenCursorParams::TIndexOpenCursorParams: { const IndexOpenCursorParams& params = aParams.get_IndexOpenCursorParams(); objectStoreMetadata = GetMetadataForObjectStoreId(params.objectStoreId()); if (NS_WARN_IF(!objectStoreMetadata)) { ASSERT_UNLESS_FUZZING(); return nullptr; } indexMetadata = GetMetadataForIndexId(objectStoreMetadata, params.indexId()); if (NS_WARN_IF(!indexMetadata)) { ASSERT_UNLESS_FUZZING(); return nullptr; } if (aTrustParams && NS_WARN_IF(!VerifyRequestParams(params.optionalKeyRange()))) { ASSERT_UNLESS_FUZZING(); return nullptr; } direction = params.direction(); break; } case OpenCursorParams::TIndexOpenKeyCursorParams: { const IndexOpenKeyCursorParams& params = aParams.get_IndexOpenKeyCursorParams(); objectStoreMetadata = GetMetadataForObjectStoreId(params.objectStoreId()); if (NS_WARN_IF(!objectStoreMetadata)) { ASSERT_UNLESS_FUZZING(); return nullptr; } indexMetadata = GetMetadataForIndexId(objectStoreMetadata, params.indexId()); if (NS_WARN_IF(!indexMetadata)) { ASSERT_UNLESS_FUZZING(); return nullptr; } if (aTrustParams && NS_WARN_IF(!VerifyRequestParams(params.optionalKeyRange()))) { ASSERT_UNLESS_FUZZING(); return nullptr; } direction = params.direction(); break; } default: MOZ_CRASH("Should never get here!"); } if (NS_WARN_IF(mCommitOrAbortReceived)) { ASSERT_UNLESS_FUZZING(); return nullptr; } RefPtr<Cursor> actor = new Cursor(this, type, objectStoreMetadata, indexMetadata, direction); // Transfer ownership to IPDL. return actor.forget().take(); } bool TransactionBase::StartCursor(PBackgroundIDBCursorParent* aActor, const OpenCursorParams& aParams) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); MOZ_ASSERT(aParams.type() != OpenCursorParams::T__None); auto* op = static_cast<Cursor*>(aActor); if (NS_WARN_IF(!op->Start(aParams))) { return false; } return true; } bool TransactionBase::DeallocCursor(PBackgroundIDBCursorParent* aActor) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); // Transfer ownership back from IPDL. RefPtr<Cursor> actor = dont_AddRef(static_cast<Cursor*>(aActor)); return true; } /******************************************************************************* * NormalTransaction ******************************************************************************/ NormalTransaction::NormalTransaction( Database* aDatabase, TransactionBase::Mode aMode, nsTArray<RefPtr<FullObjectStoreMetadata>>& aObjectStores) : TransactionBase(aDatabase, aMode) { AssertIsOnBackgroundThread(); MOZ_ASSERT(!aObjectStores.IsEmpty()); mObjectStores.SwapElements(aObjectStores); } bool NormalTransaction::IsSameProcessActor() { AssertIsOnBackgroundThread(); PBackgroundParent* 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 = true; MaybeCommitOrAbort(); } } bool NormalTransaction::RecvDeleteMe() { AssertIsOnBackgroundThread(); MOZ_ASSERT(!IsActorDestroyed()); return PBackgroundIDBTransactionParent::Send__delete__(this); } bool NormalTransaction::RecvCommit() { AssertIsOnBackgroundThread(); return TransactionBase::RecvCommit(); } bool NormalTransaction::RecvAbort(const nsresult& aResultCode) { AssertIsOnBackgroundThread(); return TransactionBase::RecvAbort(aResultCode); } PBackgroundIDBRequestParent* NormalTransaction::AllocPBackgroundIDBRequestParent( const RequestParams& aParams) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aParams.type() != RequestParams::T__None); return AllocRequest(aParams, IsSameProcessActor()); } bool NormalTransaction::RecvPBackgroundIDBRequestConstructor( PBackgroundIDBRequestParent* aActor, const RequestParams& aParams) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); MOZ_ASSERT(aParams.type() != RequestParams::T__None); return StartRequest(aActor); } bool NormalTransaction::DeallocPBackgroundIDBRequestParent( PBackgroundIDBRequestParent* aActor) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); return DeallocRequest(aActor); } PBackgroundIDBCursorParent* NormalTransaction::AllocPBackgroundIDBCursorParent( const OpenCursorParams& aParams) { AssertIsOnBackgroundThread(); return AllocCursor(aParams, IsSameProcessActor()); } bool NormalTransaction::RecvPBackgroundIDBCursorConstructor( PBackgroundIDBCursorParent* aActor, const OpenCursorParams& aParams) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); MOZ_ASSERT(aParams.type() != OpenCursorParams::T__None); return StartCursor(aActor, aParams); } bool NormalTransaction::DeallocPBackgroundIDBCursorParent( PBackgroundIDBCursorParent* aActor) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); return DeallocCursor(aActor); } /******************************************************************************* * VersionChangeTransaction ******************************************************************************/ VersionChangeTransaction::VersionChangeTransaction( OpenDatabaseOp* aOpenDatabaseOp) : TransactionBase(aOpenDatabaseOp->mDatabase, IDBTransaction::VERSION_CHANGE) , mOpenDatabaseOp(aOpenDatabaseOp) , mActorWasAlive(false) { 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(!mActorWasAlive); MOZ_ASSERT(!IsActorDestroyed()); mActorWasAlive = true; // This reference will be absorbed by IPDL and released when the actor is // destroyed. AddRef(); } bool VersionChangeTransaction::CopyDatabaseMetadata() { AssertIsOnBackgroundThread(); MOZ_ASSERT(!mOldMetadata); const RefPtr<FullDatabaseMetadata> origMetadata = GetDatabase()->Metadata(); MOZ_ASSERT(origMetadata); RefPtr<FullDatabaseMetadata> 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 = info->mMetadata.forget(); info->mMetadata.swap(newMetadata); // Replace metadata pointers for all live databases. for (uint32_t count = info->mLiveDatabases.Length(), index = 0; index < count; index++) { info->mLiveDatabases[index]->mMetadata = info->mMetadata; } return true; } void VersionChangeTransaction::UpdateMetadata(nsresult aResult) { AssertIsOnBackgroundThread(); MOZ_ASSERT(GetDatabase()); MOZ_ASSERT(mOpenDatabaseOp); MOZ_ASSERT(!!mActorWasAlive == !!mOpenDatabaseOp->mDatabase); MOZ_ASSERT_IF(mActorWasAlive, !mOpenDatabaseOp->mDatabaseId.IsEmpty()); if (IsActorDestroyed() || !mActorWasAlive) { return; } RefPtr<FullDatabaseMetadata> oldMetadata; mOldMetadata.swap(oldMetadata); 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. for (auto objectStoreIter = info->mMetadata->mObjectStores.Iter(); !objectStoreIter.Done(); objectStoreIter.Next()) { MOZ_ASSERT(objectStoreIter.Key()); RefPtr<FullObjectStoreMetadata>& metadata = objectStoreIter.Data(); MOZ_ASSERT(metadata); if (metadata->mDeleted) { objectStoreIter.Remove(); continue; } for (auto indexIter = metadata->mIndexes.Iter(); !indexIter.Done(); indexIter.Next()) { MOZ_ASSERT(indexIter.Key()); RefPtr<FullIndexMetadata>& index = indexIter.Data(); MOZ_ASSERT(index); if (index->mDeleted) { indexIter.Remove(); } } #ifdef DEBUG metadata->mIndexes.MarkImmutable(); #endif } #ifdef DEBUG info->mMetadata->mObjectStores.MarkImmutable(); #endif } else { // Replace metadata pointers for all live databases. info->mMetadata = oldMetadata.forget(); for (uint32_t count = info->mLiveDatabases.Length(), index = 0; index < count; index++) { info->mLiveDatabases[index]->mMetadata = info->mMetadata; } } } void VersionChangeTransaction::SendCompleteNotification(nsresult aResult) { AssertIsOnBackgroundThread(); MOZ_ASSERT(mOpenDatabaseOp); MOZ_ASSERT_IF(!mActorWasAlive, NS_FAILED(mOpenDatabaseOp->mResultCode)); MOZ_ASSERT_IF(!mActorWasAlive, mOpenDatabaseOp->mState > OpenDatabaseOp::State::SendingResults); RefPtr<OpenDatabaseOp> openDatabaseOp; mOpenDatabaseOp.swap(openDatabaseOp); if (!mActorWasAlive) { return; } if (NS_FAILED(aResult) && NS_SUCCEEDED(openDatabaseOp->mResultCode)) { // 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->mResultCode = 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 = true; MaybeCommitOrAbort(); } } bool VersionChangeTransaction::RecvDeleteMe() { AssertIsOnBackgroundThread(); MOZ_ASSERT(!IsActorDestroyed()); return PBackgroundIDBVersionChangeTransactionParent::Send__delete__(this); } bool VersionChangeTransaction::RecvCommit() { AssertIsOnBackgroundThread(); return TransactionBase::RecvCommit(); } bool VersionChangeTransaction::RecvAbort(const nsresult& aResultCode) { AssertIsOnBackgroundThread(); return TransactionBase::RecvAbort(aResultCode); } bool VersionChangeTransaction::RecvCreateObjectStore( const ObjectStoreMetadata& aMetadata) { AssertIsOnBackgroundThread(); if (NS_WARN_IF(!aMetadata.id())) { ASSERT_UNLESS_FUZZING(); return false; } const RefPtr<FullDatabaseMetadata> dbMetadata = GetDatabase()->Metadata(); MOZ_ASSERT(dbMetadata); if (NS_WARN_IF(aMetadata.id() != dbMetadata->mNextObjectStoreId)) { ASSERT_UNLESS_FUZZING(); return false; } auto* foundMetadata = MetadataNameOrIdMatcher<FullObjectStoreMetadata>::Match( dbMetadata->mObjectStores, aMetadata.id(), aMetadata.name()); if (NS_WARN_IF(foundMetadata)) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(mCommitOrAbortReceived)) { ASSERT_UNLESS_FUZZING(); return false; } RefPtr<FullObjectStoreMetadata> newMetadata = new FullObjectStoreMetadata(); newMetadata->mCommonMetadata = aMetadata; newMetadata->mNextAutoIncrementId = aMetadata.autoIncrement() ? 1 : 0; newMetadata->mCommittedAutoIncrementId = newMetadata->mNextAutoIncrementId; if (NS_WARN_IF(!dbMetadata->mObjectStores.Put(aMetadata.id(), newMetadata, fallible))) { return false; } dbMetadata->mNextObjectStoreId++; RefPtr<CreateObjectStoreOp> op = new CreateObjectStoreOp(this, aMetadata); if (NS_WARN_IF(!op->Init(this))) { op->Cleanup(); return false; } op->DispatchToConnectionPool(); return true; } bool VersionChangeTransaction::RecvDeleteObjectStore(const int64_t& aObjectStoreId) { AssertIsOnBackgroundThread(); if (NS_WARN_IF(!aObjectStoreId)) { ASSERT_UNLESS_FUZZING(); return false; } const RefPtr<FullDatabaseMetadata> dbMetadata = GetDatabase()->Metadata(); MOZ_ASSERT(dbMetadata); MOZ_ASSERT(dbMetadata->mNextObjectStoreId > 0); if (NS_WARN_IF(aObjectStoreId >= dbMetadata->mNextObjectStoreId)) { ASSERT_UNLESS_FUZZING(); return false; } RefPtr<FullObjectStoreMetadata> foundMetadata = GetMetadataForObjectStoreId(aObjectStoreId); if (NS_WARN_IF(!foundMetadata)) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(mCommitOrAbortReceived)) { ASSERT_UNLESS_FUZZING(); return false; } foundMetadata->mDeleted = true; bool isLastObjectStore = true; DebugOnly<bool> foundTargetId = false; for (auto iter = dbMetadata->mObjectStores.Iter(); !iter.Done(); iter.Next()) { if (uint64_t(aObjectStoreId) == iter.Key()) { foundTargetId = true; } else if (!iter.UserData()->mDeleted) { isLastObjectStore = false; break; } } MOZ_ASSERT_IF(isLastObjectStore, foundTargetId); RefPtr<DeleteObjectStoreOp> op = new DeleteObjectStoreOp(this, foundMetadata, isLastObjectStore); if (NS_WARN_IF(!op->Init(this))) { op->Cleanup(); return false; } op->DispatchToConnectionPool(); return true; } bool VersionChangeTransaction::RecvRenameObjectStore(const int64_t& aObjectStoreId, const nsString& aName) { AssertIsOnBackgroundThread(); if (NS_WARN_IF(!aObjectStoreId)) { ASSERT_UNLESS_FUZZING(); return false; } const RefPtr<FullDatabaseMetadata> dbMetadata = GetDatabase()->Metadata(); MOZ_ASSERT(dbMetadata); MOZ_ASSERT(dbMetadata->mNextObjectStoreId > 0); if (NS_WARN_IF(aObjectStoreId >= dbMetadata->mNextObjectStoreId)) { ASSERT_UNLESS_FUZZING(); return false; } RefPtr<FullObjectStoreMetadata> foundMetadata = GetMetadataForObjectStoreId(aObjectStoreId); if (NS_WARN_IF(!foundMetadata)) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(mCommitOrAbortReceived)) { ASSERT_UNLESS_FUZZING(); return false; } foundMetadata->mCommonMetadata.name() = aName; RefPtr<RenameObjectStoreOp> renameOp = new RenameObjectStoreOp(this, foundMetadata); if (NS_WARN_IF(!renameOp->Init(this))) { renameOp->Cleanup(); return false; } renameOp->DispatchToConnectionPool(); return true; } bool VersionChangeTransaction::RecvCreateIndex(const int64_t& aObjectStoreId, const IndexMetadata& aMetadata) { AssertIsOnBackgroundThread(); if (NS_WARN_IF(!aObjectStoreId)) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(!aMetadata.id())) { ASSERT_UNLESS_FUZZING(); return false; } const RefPtr<FullDatabaseMetadata> dbMetadata = GetDatabase()->Metadata(); MOZ_ASSERT(dbMetadata); if (NS_WARN_IF(aMetadata.id() != dbMetadata->mNextIndexId)) { ASSERT_UNLESS_FUZZING(); return false; } RefPtr<FullObjectStoreMetadata> foundObjectStoreMetadata = GetMetadataForObjectStoreId(aObjectStoreId); if (NS_WARN_IF(!foundObjectStoreMetadata)) { ASSERT_UNLESS_FUZZING(); return false; } RefPtr<FullIndexMetadata> foundIndexMetadata = MetadataNameOrIdMatcher<FullIndexMetadata>::Match( foundObjectStoreMetadata->mIndexes, aMetadata.id(), aMetadata.name()); if (NS_WARN_IF(foundIndexMetadata)) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(mCommitOrAbortReceived)) { ASSERT_UNLESS_FUZZING(); return false; } RefPtr<FullIndexMetadata> newMetadata = new FullIndexMetadata(); newMetadata->mCommonMetadata = aMetadata; if (NS_WARN_IF(!foundObjectStoreMetadata->mIndexes.Put(aMetadata.id(), newMetadata, fallible))) { return false; } dbMetadata->mNextIndexId++; RefPtr<CreateIndexOp> op = new CreateIndexOp(this, aObjectStoreId, aMetadata); if (NS_WARN_IF(!op->Init(this))) { op->Cleanup(); return false; } op->DispatchToConnectionPool(); return true; } bool VersionChangeTransaction::RecvDeleteIndex(const int64_t& aObjectStoreId, const int64_t& aIndexId) { AssertIsOnBackgroundThread(); if (NS_WARN_IF(!aObjectStoreId)) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(!aIndexId)) { ASSERT_UNLESS_FUZZING(); return false; } const RefPtr<FullDatabaseMetadata> dbMetadata = GetDatabase()->Metadata(); MOZ_ASSERT(dbMetadata); MOZ_ASSERT(dbMetadata->mNextObjectStoreId > 0); MOZ_ASSERT(dbMetadata->mNextIndexId > 0); if (NS_WARN_IF(aObjectStoreId >= dbMetadata->mNextObjectStoreId)) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(aIndexId >= dbMetadata->mNextIndexId)) { ASSERT_UNLESS_FUZZING(); return false; } RefPtr<FullObjectStoreMetadata> foundObjectStoreMetadata = GetMetadataForObjectStoreId(aObjectStoreId); if (NS_WARN_IF(!foundObjectStoreMetadata)) { ASSERT_UNLESS_FUZZING(); return false; } RefPtr<FullIndexMetadata> foundIndexMetadata = GetMetadataForIndexId(foundObjectStoreMetadata, aIndexId); if (NS_WARN_IF(!foundIndexMetadata)) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(mCommitOrAbortReceived)) { ASSERT_UNLESS_FUZZING(); return false; } foundIndexMetadata->mDeleted = true; bool isLastIndex = true; DebugOnly<bool> foundTargetId = false; for (auto iter = foundObjectStoreMetadata->mIndexes.ConstIter(); !iter.Done(); iter.Next()) { if (uint64_t(aIndexId) == iter.Key()) { foundTargetId = true; } else if (!iter.UserData()->mDeleted) { isLastIndex = false; break; } } MOZ_ASSERT_IF(isLastIndex, foundTargetId); RefPtr<DeleteIndexOp> op = new DeleteIndexOp(this, aObjectStoreId, aIndexId, foundIndexMetadata->mCommonMetadata.unique(), isLastIndex); if (NS_WARN_IF(!op->Init(this))) { op->Cleanup(); return false; } op->DispatchToConnectionPool(); return true; } bool VersionChangeTransaction::RecvRenameIndex(const int64_t& aObjectStoreId, const int64_t& aIndexId, const nsString& aName) { AssertIsOnBackgroundThread(); if (NS_WARN_IF(!aObjectStoreId)) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(!aIndexId)) { ASSERT_UNLESS_FUZZING(); return false; } const RefPtr<FullDatabaseMetadata> dbMetadata = GetDatabase()->Metadata(); MOZ_ASSERT(dbMetadata); MOZ_ASSERT(dbMetadata->mNextObjectStoreId > 0); MOZ_ASSERT(dbMetadata->mNextIndexId > 0); if (NS_WARN_IF(aObjectStoreId >= dbMetadata->mNextObjectStoreId)) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(aIndexId >= dbMetadata->mNextIndexId)) { ASSERT_UNLESS_FUZZING(); return false; } RefPtr<FullObjectStoreMetadata> foundObjectStoreMetadata = GetMetadataForObjectStoreId(aObjectStoreId); if (NS_WARN_IF(!foundObjectStoreMetadata)) { ASSERT_UNLESS_FUZZING(); return false; } RefPtr<FullIndexMetadata> foundIndexMetadata = GetMetadataForIndexId(foundObjectStoreMetadata, aIndexId); if (NS_WARN_IF(!foundIndexMetadata)) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(mCommitOrAbortReceived)) { ASSERT_UNLESS_FUZZING(); return false; } foundIndexMetadata->mCommonMetadata.name() = aName; RefPtr<RenameIndexOp> renameOp = new RenameIndexOp(this, foundIndexMetadata, aObjectStoreId); if (NS_WARN_IF(!renameOp->Init(this))) { renameOp->Cleanup(); return false; } renameOp->DispatchToConnectionPool(); return true; } PBackgroundIDBRequestParent* VersionChangeTransaction::AllocPBackgroundIDBRequestParent( const RequestParams& aParams) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aParams.type() != RequestParams::T__None); return AllocRequest(aParams, IsSameProcessActor()); } bool VersionChangeTransaction::RecvPBackgroundIDBRequestConstructor( PBackgroundIDBRequestParent* aActor, const RequestParams& aParams) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); MOZ_ASSERT(aParams.type() != RequestParams::T__None); return StartRequest(aActor); } bool VersionChangeTransaction::DeallocPBackgroundIDBRequestParent( PBackgroundIDBRequestParent* aActor) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); return DeallocRequest(aActor); } PBackgroundIDBCursorParent* VersionChangeTransaction::AllocPBackgroundIDBCursorParent( const OpenCursorParams& aParams) { AssertIsOnBackgroundThread(); return AllocCursor(aParams, IsSameProcessActor()); } bool VersionChangeTransaction::RecvPBackgroundIDBCursorConstructor( PBackgroundIDBCursorParent* aActor, const OpenCursorParams& aParams) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); MOZ_ASSERT(aParams.type() != OpenCursorParams::T__None); return StartCursor(aActor, aParams); } bool VersionChangeTransaction::DeallocPBackgroundIDBCursorParent( PBackgroundIDBCursorParent* aActor) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); return DeallocCursor(aActor); } /******************************************************************************* * Cursor ******************************************************************************/ Cursor::Cursor(TransactionBase* aTransaction, Type aType, FullObjectStoreMetadata* aObjectStoreMetadata, FullIndexMetadata* aIndexMetadata, Direction aDirection) : mTransaction(aTransaction) , mBackgroundParent(nullptr) , mObjectStoreMetadata(aObjectStoreMetadata) , mIndexMetadata(aIndexMetadata) , mObjectStoreId(aObjectStoreMetadata->mCommonMetadata.id()) , mIndexId(aIndexMetadata ? aIndexMetadata->mCommonMetadata.id() : 0) , mCurrentlyRunningOp(nullptr) , mType(aType) , mDirection(aDirection) , mUniqueIndex(aIndexMetadata ? aIndexMetadata->mCommonMetadata.unique() : false) , mIsSameProcessActor(!BackgroundParent::IsOtherProcessActor( aTransaction->GetBackgroundParent())) , mActorDestroyed(false) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aTransaction); MOZ_ASSERT(aType != OpenCursorParams::T__None); MOZ_ASSERT(aObjectStoreMetadata); MOZ_ASSERT_IF(aType == OpenCursorParams::TIndexOpenCursorParams || aType == OpenCursorParams::TIndexOpenKeyCursorParams, aIndexMetadata); if (mType == OpenCursorParams::TObjectStoreOpenCursorParams || mType == OpenCursorParams::TIndexOpenCursorParams) { mDatabase = aTransaction->GetDatabase(); MOZ_ASSERT(mDatabase); mFileManager = mDatabase->GetFileManager(); MOZ_ASSERT(mFileManager); mBackgroundParent = aTransaction->GetBackgroundParent(); MOZ_ASSERT(mBackgroundParent); } if (aIndexMetadata) { mLocale = aIndexMetadata->mCommonMetadata.locale(); } static_assert(OpenCursorParams::T__None == 0 && OpenCursorParams::T__Last == 4, "Lots of code here assumes only four types of cursors!"); } bool Cursor::VerifyRequestParams(const CursorRequestParams& aParams) const { AssertIsOnBackgroundThread(); MOZ_ASSERT(aParams.type() != CursorRequestParams::T__None); MOZ_ASSERT(mObjectStoreMetadata); MOZ_ASSERT_IF(mType == OpenCursorParams::TIndexOpenCursorParams || mType == OpenCursorParams::TIndexOpenKeyCursorParams, mIndexMetadata); #ifdef DEBUG { RefPtr<FullObjectStoreMetadata> objectStoreMetadata = mTransaction->GetMetadataForObjectStoreId(mObjectStoreId); if (objectStoreMetadata) { MOZ_ASSERT(objectStoreMetadata == mObjectStoreMetadata); } else { MOZ_ASSERT(mObjectStoreMetadata->mDeleted); } if (objectStoreMetadata && (mType == OpenCursorParams::TIndexOpenCursorParams || mType == OpenCursorParams::TIndexOpenKeyCursorParams)) { RefPtr<FullIndexMetadata> indexMetadata = mTransaction->GetMetadataForIndexId(objectStoreMetadata, mIndexId); if (indexMetadata) { MOZ_ASSERT(indexMetadata == mIndexMetadata); } else { MOZ_ASSERT(mIndexMetadata->mDeleted); } } } #endif if (NS_WARN_IF(mObjectStoreMetadata->mDeleted) || (mIndexMetadata && NS_WARN_IF(mIndexMetadata->mDeleted))) { ASSERT_UNLESS_FUZZING(); return false; } const Key& sortKey = IsLocaleAware() ? mSortKey : mKey; switch (aParams.type()) { case CursorRequestParams::TContinueParams: { const Key& key = aParams.get_ContinueParams().key(); if (!key.IsUnset()) { switch (mDirection) { case IDBCursor::NEXT: case IDBCursor::NEXT_UNIQUE: if (NS_WARN_IF(key <= sortKey)) { ASSERT_UNLESS_FUZZING(); return false; } break; case IDBCursor::PREV: case IDBCursor::PREV_UNIQUE: if (NS_WARN_IF(key >= sortKey)) { ASSERT_UNLESS_FUZZING(); return false; } break; default: MOZ_CRASH("Should never get here!"); } } break; } case CursorRequestParams::TContinuePrimaryKeyParams: { 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 IDBCursor::NEXT: if (NS_WARN_IF(key < sortKey || (key == sortKey && primaryKey <= mObjectKey))) { ASSERT_UNLESS_FUZZING(); return false; } break; case IDBCursor::PREV: if (NS_WARN_IF(key > sortKey || (key == sortKey && primaryKey >= mObjectKey))) { ASSERT_UNLESS_FUZZING(); return false; } break; default: MOZ_CRASH("Should never get here!"); } break; } case CursorRequestParams::TAdvanceParams: if (NS_WARN_IF(!aParams.get_AdvanceParams().count())) { ASSERT_UNLESS_FUZZING(); return false; } break; default: MOZ_CRASH("Should never get here!"); } return true; } bool Cursor::Start(const OpenCursorParams& aParams) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aParams.type() == mType); MOZ_ASSERT(!mActorDestroyed); if (NS_WARN_IF(mCurrentlyRunningOp)) { ASSERT_UNLESS_FUZZING(); return false; } const OptionalKeyRange& optionalKeyRange = mType == OpenCursorParams::TObjectStoreOpenCursorParams ? aParams.get_ObjectStoreOpenCursorParams().optionalKeyRange() : mType == OpenCursorParams::TObjectStoreOpenKeyCursorParams ? aParams.get_ObjectStoreOpenKeyCursorParams().optionalKeyRange() : mType == OpenCursorParams::TIndexOpenCursorParams ? aParams.get_IndexOpenCursorParams().optionalKeyRange() : aParams.get_IndexOpenKeyCursorParams().optionalKeyRange(); RefPtr<OpenOp> openOp = new OpenOp(this, optionalKeyRange); if (NS_WARN_IF(!openOp->Init(mTransaction))) { openOp->Cleanup(); return false; } openOp->DispatchToConnectionPool(); mCurrentlyRunningOp = openOp; return true; } void Cursor::SendResponseInternal( CursorResponse& aResponse, const nsTArray<FallibleTArray<StructuredCloneFile>>& 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_IF(aResponse.type() == CursorResponse::Tvoid_t, mKey.IsUnset()); MOZ_ASSERT_IF(aResponse.type() == CursorResponse::Tvoid_t, mRangeKey.IsUnset()); MOZ_ASSERT_IF(aResponse.type() == CursorResponse::Tvoid_t, mObjectKey.IsUnset()); MOZ_ASSERT_IF(aResponse.type() == CursorResponse::Tnsresult || aResponse.type() == CursorResponse::Tvoid_t || aResponse.type() == CursorResponse::TObjectStoreKeyCursorResponse || aResponse.type() == CursorResponse::TIndexKeyCursorResponse, aFiles.IsEmpty()); MOZ_ASSERT(!mActorDestroyed); MOZ_ASSERT(mCurrentlyRunningOp); for (size_t i = 0; i < aFiles.Length(); ++i) { const auto& files = aFiles[i]; if (!files.IsEmpty()) { MOZ_ASSERT(aResponse.type() == CursorResponse::TArrayOfObjectStoreCursorResponse || aResponse.type() == CursorResponse::TIndexCursorResponse); MOZ_ASSERT(mDatabase); MOZ_ASSERT(mBackgroundParent); FallibleTArray<SerializedStructuredCloneFile> serializedFiles; nsresult rv = SerializeStructuredCloneFiles(mBackgroundParent, mDatabase, files, /* aForPreprocess */ false, serializedFiles); if (NS_WARN_IF(NS_FAILED(rv))) { aResponse = ClampResultCode(rv); break; } 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::TIndexCursorResponse: MOZ_ASSERT(i == 0); serializedInfo = &aResponse.get_IndexCursorResponse().cloneInfo(); break; default: MOZ_CRASH("Should never get here!"); } MOZ_ASSERT(serializedInfo); MOZ_ASSERT(serializedInfo->files().IsEmpty()); serializedInfo->files().SwapElements(serializedFiles); } } // Work around the deleted function by casting to the base class. auto* base = static_cast<PBackgroundIDBCursorParent*>(this); if (!base->SendResponse(aResponse)) { NS_WARNING("Failed to send response!"); } mCurrentlyRunningOp = nullptr; } void Cursor::ActorDestroy(ActorDestroyReason aWhy) { AssertIsOnBackgroundThread(); MOZ_ASSERT(!mActorDestroyed); mActorDestroyed = true; if (mCurrentlyRunningOp) { mCurrentlyRunningOp->NoteActorDestroyed(); } mBackgroundParent = nullptr; mObjectStoreMetadata = nullptr; mIndexMetadata = nullptr; } bool Cursor::RecvDeleteMe() { AssertIsOnBackgroundThread(); MOZ_ASSERT(!mActorDestroyed); if (NS_WARN_IF(mCurrentlyRunningOp)) { ASSERT_UNLESS_FUZZING(); return false; } return PBackgroundIDBCursorParent::Send__delete__(this); } bool Cursor::RecvContinue(const CursorRequestParams& aParams) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aParams.type() != CursorRequestParams::T__None); MOZ_ASSERT(!mActorDestroyed); MOZ_ASSERT(mObjectStoreMetadata); MOZ_ASSERT_IF(mType == OpenCursorParams::TIndexOpenCursorParams || mType == OpenCursorParams::TIndexOpenKeyCursorParams, mIndexMetadata); const bool trustParams = #ifdef DEBUG // Always verify parameters in DEBUG builds! false #else mIsSameProcessActor #endif ; if (!trustParams && !VerifyRequestParams(aParams)) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(mCurrentlyRunningOp)) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(mTransaction->mCommitOrAbortReceived)) { ASSERT_UNLESS_FUZZING(); return false; } RefPtr<ContinueOp> continueOp = new ContinueOp(this, aParams); if (NS_WARN_IF(!continueOp->Init(mTransaction))) { continueOp->Cleanup(); return false; } continueOp->DispatchToConnectionPool(); mCurrentlyRunningOp = continueOp; return true; } /******************************************************************************* * FileManager ******************************************************************************/ FileManager::FileManager(PersistenceType aPersistenceType, const nsACString& aGroup, const nsACString& aOrigin, bool aIsApp, const nsAString& aDatabaseName, bool aEnforcingQuota) : mPersistenceType(aPersistenceType) , mGroup(aGroup) , mOrigin(aOrigin) , mDatabaseName(aDatabaseName) , mLastFileId(0) , mIsApp(aIsApp) , mEnforcingQuota(aEnforcingQuota) , mInvalidated(false) { } FileManager::~FileManager() { } nsresult FileManager::Init(nsIFile* aDirectory, mozIStorageConnection* aConnection) { AssertIsOnIOThread(); MOZ_ASSERT(aDirectory); MOZ_ASSERT(aConnection); bool exists; nsresult rv = aDirectory->Exists(&exists); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (exists) { bool isDirectory; rv = aDirectory->IsDirectory(&isDirectory); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (NS_WARN_IF(!isDirectory)) { return NS_ERROR_FAILURE; } } else { rv = aDirectory->Create(nsIFile::DIRECTORY_TYPE, 0755); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } rv = aDirectory->GetPath(mDirectoryPath); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsCOMPtr<nsIFile> journalDirectory; rv = aDirectory->Clone(getter_AddRefs(journalDirectory)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = journalDirectory->Append(NS_LITERAL_STRING(JOURNAL_DIRECTORY_NAME)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = journalDirectory->Exists(&exists); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (exists) { bool isDirectory; rv = journalDirectory->IsDirectory(&isDirectory); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (NS_WARN_IF(!isDirectory)) { return NS_ERROR_FAILURE; } } rv = journalDirectory->GetPath(mJournalDirectoryPath); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsCOMPtr<mozIStorageStatement> stmt; rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( "SELECT id, refcount " "FROM file" ), getter_AddRefs(stmt)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } bool hasResult; while (NS_SUCCEEDED((rv = stmt->ExecuteStep(&hasResult))) && hasResult) { int64_t id; rv = stmt->GetInt64(0, &id); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } int32_t refcount; rv = stmt->GetInt32(1, &refcount); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(refcount > 0); RefPtr<FileInfo> fileInfo = FileInfo::Create(this, id); fileInfo->mDBRefCnt = static_cast<nsrefcnt>(refcount); mFileInfos.Put(id, fileInfo); mLastFileId = std::max(id, mLastFileId); } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } nsresult FileManager::Invalidate() { if (IndexedDatabaseManager::IsClosed()) { MOZ_ASSERT(false, "Shouldn't be called after shutdown!"); return NS_ERROR_UNEXPECTED; } MutexAutoLock lock(IndexedDatabaseManager::FileMutex()); MOZ_ASSERT(!mInvalidated); mInvalidated = true; for (auto iter = mFileInfos.Iter(); !iter.Done(); iter.Next()) { FileInfo* info = iter.Data(); MOZ_ASSERT(info); if (!info->LockedClearDBRefs()) { iter.Remove(); } } return NS_OK; } already_AddRefed<nsIFile> FileManager::GetDirectory() { return GetFileForPath(mDirectoryPath); } already_AddRefed<nsIFile> FileManager::GetCheckedDirectory() { nsCOMPtr<nsIFile> directory = GetDirectory(); if (NS_WARN_IF(!directory)) { return nullptr; } DebugOnly<bool> exists; MOZ_ASSERT(NS_SUCCEEDED(directory->Exists(&exists))); MOZ_ASSERT(exists); DebugOnly<bool> isDirectory; MOZ_ASSERT(NS_SUCCEEDED(directory->IsDirectory(&isDirectory))); MOZ_ASSERT(isDirectory); return directory.forget(); } already_AddRefed<nsIFile> FileManager::GetJournalDirectory() { return GetFileForPath(mJournalDirectoryPath); } already_AddRefed<nsIFile> FileManager::EnsureJournalDirectory() { // This can happen on the IO or on a transaction thread. MOZ_ASSERT(!NS_IsMainThread()); nsCOMPtr<nsIFile> journalDirectory = GetFileForPath(mJournalDirectoryPath); if (NS_WARN_IF(!journalDirectory)) { return nullptr; } bool exists; nsresult rv = journalDirectory->Exists(&exists); if (NS_WARN_IF(NS_FAILED(rv))) { return nullptr; } if (exists) { bool isDirectory; rv = journalDirectory->IsDirectory(&isDirectory); if (NS_WARN_IF(NS_FAILED(rv))) { return nullptr; } if (NS_WARN_IF(!isDirectory)) { return nullptr; } } else { rv = journalDirectory->Create(nsIFile::DIRECTORY_TYPE, 0755); if (NS_WARN_IF(NS_FAILED(rv))) { return nullptr; } } return journalDirectory.forget(); } already_AddRefed<FileInfo> FileManager::GetFileInfo(int64_t aId) { if (IndexedDatabaseManager::IsClosed()) { MOZ_ASSERT(false, "Shouldn't be called after shutdown!"); return nullptr; } FileInfo* fileInfo; { MutexAutoLock lock(IndexedDatabaseManager::FileMutex()); fileInfo = mFileInfos.Get(aId); } RefPtr<FileInfo> result = fileInfo; return result.forget(); } already_AddRefed<FileInfo> FileManager::GetNewFileInfo() { MOZ_ASSERT(!IndexedDatabaseManager::IsClosed()); FileInfo* fileInfo; { MutexAutoLock lock(IndexedDatabaseManager::FileMutex()); int64_t id = mLastFileId + 1; fileInfo = FileInfo::Create(this, id); mFileInfos.Put(id, fileInfo); mLastFileId = id; } RefPtr<FileInfo> result = fileInfo; return result.forget(); } // static already_AddRefed<nsIFile> FileManager::GetFileForId(nsIFile* aDirectory, int64_t aId) { MOZ_ASSERT(aDirectory); MOZ_ASSERT(aId > 0); nsAutoString id; id.AppendInt(aId); nsCOMPtr<nsIFile> file; nsresult rv = aDirectory->Clone(getter_AddRefs(file)); if (NS_WARN_IF(NS_FAILED(rv))) { return nullptr; } rv = file->Append(id); if (NS_WARN_IF(NS_FAILED(rv))) { return nullptr; } return file.forget(); } // static already_AddRefed<nsIFile> FileManager::GetCheckedFileForId(nsIFile* aDirectory, int64_t aId) { nsCOMPtr<nsIFile> file = GetFileForId(aDirectory, aId); if (NS_WARN_IF(!file)) { return nullptr; } DebugOnly<bool> exists; MOZ_ASSERT(NS_SUCCEEDED(file->Exists(&exists))); MOZ_ASSERT(exists); DebugOnly<bool> isFile; MOZ_ASSERT(NS_SUCCEEDED(file->IsFile(&isFile))); MOZ_ASSERT(isFile); return file.forget(); } // static nsresult FileManager::InitDirectory(nsIFile* aDirectory, nsIFile* aDatabaseFile, PersistenceType aPersistenceType, const nsACString& aGroup, const nsACString& aOrigin, uint32_t aTelemetryId) { AssertIsOnIOThread(); MOZ_ASSERT(aDirectory); MOZ_ASSERT(aDatabaseFile); bool exists; nsresult rv = aDirectory->Exists(&exists); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (!exists) { return NS_OK; } bool isDirectory; rv = aDirectory->IsDirectory(&isDirectory); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (NS_WARN_IF(!isDirectory)) { return NS_ERROR_FAILURE; } nsCOMPtr<nsIFile> journalDirectory; rv = aDirectory->Clone(getter_AddRefs(journalDirectory)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = journalDirectory->Append(NS_LITERAL_STRING(JOURNAL_DIRECTORY_NAME)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = journalDirectory->Exists(&exists); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (exists) { rv = journalDirectory->IsDirectory(&isDirectory); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (NS_WARN_IF(!isDirectory)) { return NS_ERROR_FAILURE; } nsCOMPtr<nsISimpleEnumerator> entries; rv = journalDirectory->GetDirectoryEntries(getter_AddRefs(entries)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } bool hasElements; rv = entries->HasMoreElements(&hasElements); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (hasElements) { nsCOMPtr<mozIStorageConnection> connection; rv = CreateStorageConnection(aDatabaseFile, aDirectory, NullString(), aPersistenceType, aGroup, aOrigin, aTelemetryId, getter_AddRefs(connection)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } mozStorageTransaction transaction(connection, false); rv = connection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE VIRTUAL TABLE fs USING filesystem;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsCOMPtr<mozIStorageStatement> stmt; rv = connection->CreateStatement(NS_LITERAL_CSTRING( "SELECT name, (name IN (SELECT id FROM file)) " "FROM fs " "WHERE path = :path" ), getter_AddRefs(stmt)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsString path; rv = journalDirectory->GetPath(path); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->BindStringByName(NS_LITERAL_CSTRING("path"), path); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } bool hasResult; while (NS_SUCCEEDED((rv = stmt->ExecuteStep(&hasResult))) && hasResult) { nsString name; rv = stmt->GetString(0, name); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } int32_t flag = stmt->AsInt32(1); if (!flag) { nsCOMPtr<nsIFile> file; rv = aDirectory->Clone(getter_AddRefs(file)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = file->Append(name); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (NS_FAILED(file->Remove(false))) { NS_WARNING("Failed to remove orphaned file!"); } } nsCOMPtr<nsIFile> journalFile; rv = journalDirectory->Clone(getter_AddRefs(journalFile)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = journalFile->Append(name); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (NS_FAILED(journalFile->Remove(false))) { NS_WARNING("Failed to remove journal file!"); } } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = connection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TABLE fs;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = transaction.Commit(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } } return NS_OK; } // static nsresult FileManager::GetUsage(nsIFile* aDirectory, uint64_t* aUsage) { AssertIsOnIOThread(); MOZ_ASSERT(aDirectory); MOZ_ASSERT(aUsage); bool exists; nsresult rv = aDirectory->Exists(&exists); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (!exists) { *aUsage = 0; return NS_OK; } nsCOMPtr<nsISimpleEnumerator> entries; rv = aDirectory->GetDirectoryEntries(getter_AddRefs(entries)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } uint64_t usage = 0; bool hasMore; while (NS_SUCCEEDED((rv = entries->HasMoreElements(&hasMore))) && hasMore) { nsCOMPtr<nsISupports> entry; rv = entries->GetNext(getter_AddRefs(entry)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsCOMPtr<nsIFile> file = do_QueryInterface(entry); MOZ_ASSERT(file); nsString leafName; rv = file->GetLeafName(leafName); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (leafName.EqualsLiteral(JOURNAL_DIRECTORY_NAME)) { continue; } int64_t fileSize; rv = file->GetFileSize(&fileSize); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } UsageInfo::IncrementUsage(&usage, uint64_t(fileSize)); } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } *aUsage = usage; return NS_OK; } /******************************************************************************* * FileImplStoredFile ******************************************************************************/ NS_IMPL_ISUPPORTS_INHERITED(BlobImplStoredFile, BlobImplFile, BlobImplStoredFile) /******************************************************************************* * QuotaClient ******************************************************************************/ QuotaClient* QuotaClient::sInstance = nullptr; QuotaClient::QuotaClient() : mShutdownRequested(false) { 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"); 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; sInstance = nullptr; } nsThreadPool* QuotaClient::GetOrCreateThreadPool() { AssertIsOnBackgroundThread(); MOZ_ASSERT(!mShutdownRequested); if (!mMaintenanceThreadPool) { RefPtr<nsThreadPool> 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(NS_LITERAL_CSTRING("IndexedDB Mnt"))); mMaintenanceThreadPool = Move(threadPool); } return mMaintenanceThreadPool; } mozilla::dom::quota::Client::Type QuotaClient::GetType() { return QuotaClient::IDB; } struct FileManagerInitInfo { nsCOMPtr<nsIFile> mDirectory; nsCOMPtr<nsIFile> mDatabaseFile; nsCOMPtr<nsIFile> mDatabaseWALFile; }; nsresult QuotaClient::InitOrigin(PersistenceType aPersistenceType, const nsACString& aGroup, const nsACString& aOrigin, const AtomicBool& aCanceled, UsageInfo* aUsageInfo) { AssertIsOnIOThread(); nsCOMPtr<nsIFile> directory; nsresult rv = GetDirectory(aPersistenceType, aOrigin, getter_AddRefs(directory)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // 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. AutoTArray<nsString, 20> subdirsToProcess; nsTArray<nsCOMPtr<nsIFile>> unknownFiles; nsTHashtable<nsStringHashKey> validSubdirs(20); AutoTArray<FileManagerInitInfo, 20> initInfos; nsCOMPtr<nsISimpleEnumerator> entries; rv = directory->GetDirectoryEntries(getter_AddRefs(entries)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } const NS_ConvertASCIItoUTF16 filesSuffix( kFileManagerDirectoryNameSuffix, LiteralStringLength(kFileManagerDirectoryNameSuffix)); const NS_ConvertASCIItoUTF16 journalSuffix( kSQLiteJournalSuffix, LiteralStringLength(kSQLiteJournalSuffix)); const NS_ConvertASCIItoUTF16 shmSuffix(kSQLiteSHMSuffix, LiteralStringLength(kSQLiteSHMSuffix)); const NS_ConvertASCIItoUTF16 walSuffix(kSQLiteWALSuffix, LiteralStringLength(kSQLiteWALSuffix)); bool hasMore; while (NS_SUCCEEDED((rv = entries->HasMoreElements(&hasMore))) && hasMore && !aCanceled) { nsCOMPtr<nsISupports> entry; rv = entries->GetNext(getter_AddRefs(entry)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsCOMPtr<nsIFile> file = do_QueryInterface(entry); MOZ_ASSERT(file); nsString leafName; rv = file->GetLeafName(leafName); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } bool isDirectory; rv = file->IsDirectory(&isDirectory); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (isDirectory) { if (!StringEndsWith(leafName, filesSuffix) || !validSubdirs.GetEntry(leafName)) { subdirsToProcess.AppendElement(leafName); } continue; } // Skip Desktop Service Store (.DS_Store) files. These files are only used // on Mac OS X, but the profile can be shared across different operating // systems, so we check it on all platforms. if (leafName.EqualsLiteral(DSSTORE_FILE_NAME)) { continue; } // 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, journalSuffix) || StringEndsWith(leafName, shmSuffix)) { continue; } // The SQLite WAL file does count towards quota, but it is handled below // once we find the actual database file. if (StringEndsWith(leafName, walSuffix)) { continue; } nsDependentSubstring dbBaseFilename; if (!GetDatabaseBaseFilename(leafName, dbBaseFilename)) { unknownFiles.AppendElement(file); continue; } nsCOMPtr<nsIFile> fmDirectory; rv = directory->Clone(getter_AddRefs(fmDirectory)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsString fmDirectoryBaseName = dbBaseFilename + filesSuffix; rv = fmDirectory->Append(fmDirectoryBaseName); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsCOMPtr<nsIFile> walFile; if (aUsageInfo) { rv = directory->Clone(getter_AddRefs(walFile)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = walFile->Append(dbBaseFilename + walSuffix); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } FileManagerInitInfo* initInfo = initInfos.AppendElement(); initInfo->mDirectory.swap(fmDirectory); initInfo->mDatabaseFile.swap(file); initInfo->mDatabaseWALFile.swap(walFile); validSubdirs.PutEntry(fmDirectoryBaseName); } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } for (uint32_t count = subdirsToProcess.Length(), i = 0; i < count; i++) { const nsString& subdirName = subdirsToProcess[i]; // If the directory has the correct suffix then it must exist in // validSubdirs. if (StringEndsWith(subdirName, filesSuffix)) { if (NS_WARN_IF(!validSubdirs.GetEntry(subdirName))) { return NS_ERROR_UNEXPECTED; } continue; } // 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. nsString subdirNameWithSuffix = subdirName + filesSuffix; if (!validSubdirs.GetEntry(subdirNameWithSuffix)) { // 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. subdirNameWithSuffix = subdirName + NS_LITERAL_STRING(".") + filesSuffix; if (NS_WARN_IF(!validSubdirs.GetEntry(subdirNameWithSuffix))) { return NS_ERROR_UNEXPECTED; } } // We do have a database that uses this directory so we should rename it // now. However, first check to make sure that we're not overwriting // something else. nsCOMPtr<nsIFile> subdir; rv = directory->Clone(getter_AddRefs(subdir)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = subdir->Append(subdirNameWithSuffix); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } bool exists; rv = subdir->Exists(&exists); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (exists) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } rv = directory->Clone(getter_AddRefs(subdir)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = subdir->Append(subdirName); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } DebugOnly<bool> isDirectory; MOZ_ASSERT(NS_SUCCEEDED(subdir->IsDirectory(&isDirectory))); MOZ_ASSERT(isDirectory); rv = subdir->RenameTo(nullptr, subdirNameWithSuffix); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } for (uint32_t count = initInfos.Length(), i = 0; i < count && !aCanceled; i++) { FileManagerInitInfo& initInfo = initInfos[i]; MOZ_ASSERT(initInfo.mDirectory); MOZ_ASSERT(initInfo.mDatabaseFile); MOZ_ASSERT_IF(aUsageInfo, initInfo.mDatabaseWALFile); rv = FileManager::InitDirectory(initInfo.mDirectory, initInfo.mDatabaseFile, aPersistenceType, aGroup, aOrigin, TelemetryIdForFile(initInfo.mDatabaseFile)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (aUsageInfo) { int64_t fileSize; rv = initInfo.mDatabaseFile->GetFileSize(&fileSize); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(fileSize >= 0); aUsageInfo->AppendToDatabaseUsage(uint64_t(fileSize)); rv = initInfo.mDatabaseWALFile->GetFileSize(&fileSize); if (NS_SUCCEEDED(rv)) { MOZ_ASSERT(fileSize >= 0); aUsageInfo->AppendToDatabaseUsage(uint64_t(fileSize)); } else if (NS_WARN_IF(rv != NS_ERROR_FILE_NOT_FOUND && rv != NS_ERROR_FILE_TARGET_DOES_NOT_EXIST)) { return rv; } uint64_t usage; rv = FileManager::GetUsage(initInfo.mDirectory, &usage); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } aUsageInfo->AppendToFileUsage(usage); } } // We have to do this after file manager initialization. if (!unknownFiles.IsEmpty()) { #ifdef DEBUG for (uint32_t count = unknownFiles.Length(), i = 0; i < count; i++) { nsCOMPtr<nsIFile>& unknownFile = unknownFiles[i]; nsString leafName; MOZ_ALWAYS_SUCCEEDS(unknownFile->GetLeafName(leafName)); MOZ_ASSERT(!StringEndsWith(leafName, journalSuffix)); MOZ_ASSERT(!StringEndsWith(leafName, shmSuffix)); MOZ_ASSERT(!StringEndsWith(leafName, walSuffix)); nsString path; MOZ_ALWAYS_SUCCEEDS(unknownFile->GetPath(path)); MOZ_ASSERT(!path.IsEmpty()); nsPrintfCString warning("Refusing to open databases for \"%s\" because " "an unexpected file exists in the storage " "area: \"%s\"", PromiseFlatCString(aOrigin).get(), NS_ConvertUTF16toUTF8(path).get()); NS_WARNING(warning.get()); } #endif return NS_ERROR_UNEXPECTED; } return NS_OK; } nsresult QuotaClient::GetUsageForOrigin(PersistenceType aPersistenceType, const nsACString& aGroup, const nsACString& aOrigin, const AtomicBool& aCanceled, UsageInfo* aUsageInfo) { AssertIsOnIOThread(); MOZ_ASSERT(aUsageInfo); nsCOMPtr<nsIFile> directory; nsresult rv = GetDirectory(aPersistenceType, aOrigin, getter_AddRefs(directory)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = GetUsageForDirectoryInternal(directory, aCanceled, aUsageInfo, true); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } void QuotaClient::OnOriginClearCompleted(PersistenceType aPersistenceType, const nsACString& aOrigin) { AssertIsOnIOThread(); if (IndexedDatabaseManager* mgr = IndexedDatabaseManager::Get()) { mgr->InvalidateFileManagers(aPersistenceType, aOrigin); } } void QuotaClient::ReleaseIOThreadObjects() { AssertIsOnIOThread(); if (IndexedDatabaseManager* mgr = IndexedDatabaseManager::Get()) { mgr->InvalidateAllFileManagers(); } } void QuotaClient::AbortOperations(const nsACString& aOrigin) { AssertIsOnBackgroundThread(); if (!gLiveDatabaseHashtable) { return; } nsTArray<RefPtr<Database>> databases; for (auto iter = gLiveDatabaseHashtable->ConstIter(); !iter.Done(); iter.Next()) { for (Database* database : iter.Data()->mLiveDatabases) { if (aOrigin.IsVoid() || database->Origin() == aOrigin) { databases.AppendElement(database); } } } for (Database* database : databases) { database->Invalidate(); } databases.Clear(); } void QuotaClient::AbortOperationsForProcess(ContentParentId aContentParentId) { AssertIsOnBackgroundThread(); if (!gLiveDatabaseHashtable) { return; } nsTArray<RefPtr<Database>> databases; for (auto iter = gLiveDatabaseHashtable->ConstIter(); !iter.Done(); iter.Next()) { for (Database* database : iter.Data()->mLiveDatabases) { if (database->IsOwnedByProcess(aContentParentId)) { databases.AppendElement(database); } } } for (Database* database : databases) { database->Invalidate(); } databases.Clear(); } void QuotaClient::StartIdleMaintenance() { AssertIsOnBackgroundThread(); MOZ_ASSERT(!mShutdownRequested); mBackgroundThread = do_GetCurrentThread(); RefPtr<Maintenance> maintenance = new Maintenance(this); mMaintenanceQueue.AppendElement(maintenance.forget()); ProcessMaintenanceQueue(); } void QuotaClient::StopIdleMaintenance() { AssertIsOnBackgroundThread(); MOZ_ASSERT(!mShutdownRequested); if (mCurrentMaintenance) { mCurrentMaintenance->Abort(); } for (RefPtr<Maintenance>& maintenance : mMaintenanceQueue) { maintenance->Abort(); } } void QuotaClient::ShutdownWorkThreads() { AssertIsOnBackgroundThread(); MOZ_ASSERT(!mShutdownRequested); mShutdownRequested = true; // Shutdown maintenance thread pool (this spins the event loop until all // threads are gone). This should release any maintenance related quota // objects. if (mMaintenanceThreadPool) { mMaintenanceThreadPool->Shutdown(); mMaintenanceThreadPool = nullptr; } // Let any runnables dispatched from dying maintenance threads to be // processed. This should release any maintenance related directory locks. if (mCurrentMaintenance) { nsIThread* currentThread = NS_GetCurrentThread(); MOZ_ASSERT(currentThread); do { MOZ_ALWAYS_TRUE(NS_ProcessNextEvent(currentThread)); } while (!mCurrentMaintenance); } RefPtr<ConnectionPool> connectionPool = gConnectionPool.get(); if (connectionPool) { connectionPool->Shutdown(); gConnectionPool = nullptr; } RefPtr<FileHandleThreadPool> fileHandleThreadPool = gFileHandleThreadPool.get(); if (fileHandleThreadPool) { fileHandleThreadPool->Shutdown(); gFileHandleThreadPool = nullptr; } } void QuotaClient::DidInitialize(QuotaManager* aQuotaManager) { MOZ_ASSERT(NS_IsMainThread()); if (IndexedDatabaseManager* mgr = IndexedDatabaseManager::Get()) { mgr->NoteLiveQuotaManager(aQuotaManager); } } void QuotaClient::WillShutdown() { MOZ_ASSERT(NS_IsMainThread()); if (IndexedDatabaseManager* mgr = IndexedDatabaseManager::Get()) { mgr->NoteShuttingDownQuotaManager(); } } nsresult QuotaClient::GetDirectory(PersistenceType aPersistenceType, const nsACString& aOrigin, nsIFile** aDirectory) { QuotaManager* quotaManager = QuotaManager::Get(); NS_ASSERTION(quotaManager, "This should never fail!"); nsCOMPtr<nsIFile> directory; nsresult rv = quotaManager->GetDirectoryForOrigin(aPersistenceType, aOrigin, getter_AddRefs(directory)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(directory); rv = directory->Append(NS_LITERAL_STRING(IDB_DIRECTORY_NAME)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } directory.forget(aDirectory); return NS_OK; } nsresult QuotaClient::GetUsageForDirectoryInternal(nsIFile* aDirectory, const AtomicBool& aCanceled, UsageInfo* aUsageInfo, bool aDatabaseFiles) { AssertIsOnIOThread(); MOZ_ASSERT(aDirectory); MOZ_ASSERT(aUsageInfo); nsCOMPtr<nsISimpleEnumerator> entries; nsresult rv = aDirectory->GetDirectoryEntries(getter_AddRefs(entries)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (!entries) { return NS_OK; } const NS_ConvertASCIItoUTF16 journalSuffix( kSQLiteJournalSuffix, LiteralStringLength(kSQLiteJournalSuffix)); const NS_ConvertASCIItoUTF16 shmSuffix(kSQLiteSHMSuffix, LiteralStringLength(kSQLiteSHMSuffix)); bool hasMore; while (NS_SUCCEEDED((rv = entries->HasMoreElements(&hasMore))) && hasMore && !aCanceled) { nsCOMPtr<nsISupports> entry; rv = entries->GetNext(getter_AddRefs(entry)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsCOMPtr<nsIFile> file = do_QueryInterface(entry); MOZ_ASSERT(file); nsString leafName; rv = file->GetLeafName(leafName); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Journal files and sqlite-shm files don't count towards usage. if (StringEndsWith(leafName, journalSuffix) || StringEndsWith(leafName, shmSuffix)) { continue; } bool isDirectory; rv = file->IsDirectory(&isDirectory); if (rv == NS_ERROR_FILE_NOT_FOUND || rv == NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) { continue; } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (isDirectory) { if (aDatabaseFiles) { rv = GetUsageForDirectoryInternal(file, aCanceled, aUsageInfo, false); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } else { nsString leafName; rv = file->GetLeafName(leafName); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (!leafName.EqualsLiteral(JOURNAL_DIRECTORY_NAME)) { NS_WARNING("Unknown directory found!"); } } continue; } int64_t fileSize; rv = file->GetFileSize(&fileSize); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(fileSize >= 0); if (aDatabaseFiles) { aUsageInfo->AppendToDatabaseUsage(uint64_t(fileSize)); } else { aUsageInfo->AppendToFileUsage(uint64_t(fileSize)); } } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } void QuotaClient::ProcessMaintenanceQueue() { AssertIsOnBackgroundThread(); if (mCurrentMaintenance || mMaintenanceQueue.IsEmpty()) { return; } mCurrentMaintenance = mMaintenanceQueue[0]; mMaintenanceQueue.RemoveElementAt(0); mCurrentMaintenance->RunImmediately(); } void Maintenance::RegisterDatabaseMaintenance( DatabaseMaintenance* aDatabaseMaintenance) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aDatabaseMaintenance); MOZ_ASSERT(mState == State::BeginDatabaseMaintenance); MOZ_ASSERT(!mDatabaseMaintenances.Get(aDatabaseMaintenance->DatabasePath())); mDatabaseMaintenances.Put(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(); } 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* 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; } // Get a shared lock for <profile>/storage/*/*/idb mState = State::DirectoryOpenPending; QuotaManager::Get()->OpenDirectoryInternal( Nullable<PersistenceType>(), OriginScope::FromNull(), Nullable<Client::Type>(Client::IDB), /* aExclusive */ false, this); 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* quotaManager = QuotaManager::Get(); MOZ_ASSERT(quotaManager); mState = State::DirectoryWorkOpen; nsresult rv = quotaManager->IOThread()->Dispatch(this, NS_DISPATCH_NORMAL); if (NS_WARN_IF(NS_FAILED(rv))) { return NS_ERROR_FAILURE; } return NS_OK; } nsresult Maintenance::DirectoryWork() { AssertIsOnIOThread(); MOZ_ASSERT(mState == State::DirectoryWorkOpen); // The storage directory is structured like this: // // <profile>/storage/<persistence>/<origin>/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* quotaManager = QuotaManager::Get(); MOZ_ASSERT(quotaManager); nsresult rv = quotaManager->EnsureStorageIsInitialized(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsCOMPtr<nsIFile> storageDir = GetFileForPath(quotaManager->GetStoragePath()); if (NS_WARN_IF(!storageDir)) { return NS_ERROR_FAILURE; } bool exists; rv = storageDir->Exists(&exists); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (!exists) { return NS_ERROR_NOT_AVAILABLE; } bool isDirectory; rv = storageDir->IsDirectory(&isDirectory); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (NS_WARN_IF(!isDirectory)) { return NS_ERROR_FAILURE; } // There are currently only 3 persistence types, and we want to iterate them // in this order: static const PersistenceType kPersistenceTypes[] = { PERSISTENCE_TYPE_PERSISTENT, PERSISTENCE_TYPE_DEFAULT, PERSISTENCE_TYPE_TEMPORARY }; static_assert((sizeof(kPersistenceTypes) / sizeof(kPersistenceTypes[0])) == size_t(PERSISTENCE_TYPE_INVALID), "Something changed with available persistence types!"); NS_NAMED_LITERAL_STRING(idbDirName, IDB_DIRECTORY_NAME); NS_NAMED_LITERAL_STRING(sqliteExtension, ".sqlite"); for (const PersistenceType persistenceType : kPersistenceTypes) { // Loop over "<persistence>" directories. if (IsAborted()) { return NS_ERROR_ABORT; } nsAutoCString persistenceTypeString; if (persistenceType == PERSISTENCE_TYPE_PERSISTENT) { // XXX This shouldn't be a special case... persistenceTypeString.AssignLiteral("permanent"); } else { PersistenceTypeToText(persistenceType, persistenceTypeString); } nsCOMPtr<nsIFile> persistenceDir; rv = storageDir->Clone(getter_AddRefs(persistenceDir)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = persistenceDir->Append(NS_ConvertASCIItoUTF16(persistenceTypeString)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = persistenceDir->Exists(&exists); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (!exists) { continue; } rv = persistenceDir->IsDirectory(&isDirectory); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (NS_WARN_IF(!isDirectory)) { continue; } nsCOMPtr<nsISimpleEnumerator> persistenceDirEntries; rv = persistenceDir->GetDirectoryEntries( getter_AddRefs(persistenceDirEntries)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (!persistenceDirEntries) { continue; } while (true) { // Loop over "<origin>/idb" directories. if (IsAborted()) { return NS_ERROR_ABORT; } bool persistenceDirHasMoreEntries; rv = persistenceDirEntries->HasMoreElements( &persistenceDirHasMoreEntries); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (!persistenceDirHasMoreEntries) { break; } nsCOMPtr<nsISupports> persistenceDirEntry; rv = persistenceDirEntries->GetNext(getter_AddRefs(persistenceDirEntry)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsCOMPtr<nsIFile> originDir = do_QueryInterface(persistenceDirEntry); MOZ_ASSERT(originDir); rv = originDir->Exists(&exists); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(exists); rv = originDir->IsDirectory(&isDirectory); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (!isDirectory) { continue; } nsCOMPtr<nsIFile> idbDir; rv = originDir->Clone(getter_AddRefs(idbDir)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = idbDir->Append(idbDirName); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = idbDir->Exists(&exists); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (!exists) { continue; } rv = idbDir->IsDirectory(&isDirectory); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (NS_WARN_IF(!isDirectory)) { continue; } nsCOMPtr<nsISimpleEnumerator> idbDirEntries; rv = idbDir->GetDirectoryEntries(getter_AddRefs(idbDirEntries)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (!idbDirEntries) { continue; } nsCString suffix; nsCString group; nsCString origin; bool isApp; nsTArray<nsString> databasePaths; while (true) { // Loop over files in the "idb" directory. if (IsAborted()) { return NS_ERROR_ABORT; } bool idbDirHasMoreEntries; rv = idbDirEntries->HasMoreElements(&idbDirHasMoreEntries); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (!idbDirHasMoreEntries) { break; } nsCOMPtr<nsISupports> idbDirEntry; rv = idbDirEntries->GetNext(getter_AddRefs(idbDirEntry)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsCOMPtr<nsIFile> idbDirFile = do_QueryInterface(idbDirEntry); MOZ_ASSERT(idbDirFile); nsString idbFilePath; rv = idbDirFile->GetPath(idbFilePath); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (!StringEndsWith(idbFilePath, sqliteExtension)) { continue; } rv = idbDirFile->Exists(&exists); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(exists); rv = idbDirFile->IsDirectory(&isDirectory); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (isDirectory) { continue; } // Found a database. if (databasePaths.IsEmpty()) { MOZ_ASSERT(suffix.IsEmpty()); MOZ_ASSERT(group.IsEmpty()); MOZ_ASSERT(origin.IsEmpty()); int64_t dummyTimeStamp; if (NS_WARN_IF(NS_FAILED( quotaManager->GetDirectoryMetadata2(originDir, &dummyTimeStamp, suffix, group, origin, &isApp)))) { // Not much we can do here... continue; } } MOZ_ASSERT(!databasePaths.Contains(idbFilePath)); databasePaths.AppendElement(idbFilePath); } if (!databasePaths.IsEmpty()) { mDirectoryInfos.AppendElement(DirectoryInfo(persistenceType, group, origin, Move(databasePaths))); nsCOMPtr<nsIFile> directory; // Idle maintenance may occur before origin is initailized. // Ensure origin is initialized first. It will initialize all origins // for temporary storage including IDB origins. rv = quotaManager->EnsureOriginIsInitialized(persistenceType, suffix, group, origin, isApp, getter_AddRefs(directory)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } } } 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--) { RefPtr<FactoryOp>& existingOp = (*gFactoryOps)[index - 1]; MOZ_ASSERT(!existingOp->DatabaseFilePath().IsEmpty()); if (existingOp->DatabaseFilePath() == aDatabasePath) { return false; } } } if (gLiveDatabaseHashtable) { for (auto iter = gLiveDatabaseHashtable->ConstIter(); !iter.Done(); iter.Next()) { for (Database* database : iter.Data()->mLiveDatabases) { if (database->FilePath() == aDatabasePath) { return false; } } } } return true; } }; if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || IsAborted()) { return NS_ERROR_ABORT; } RefPtr<nsThreadPool> threadPool; for (DirectoryInfo& directoryInfo : mDirectoryInfos) { for (const nsString& databasePath : directoryInfo.mDatabasePaths) { if (Helper::IsSafeToRunMaintenance(databasePath)) { RefPtr<DatabaseMaintenance> databaseMaintenance = new DatabaseMaintenance(this, directoryInfo.mPersistenceType, directoryInfo.mGroup, directoryInfo.mOrigin, databasePath); if (!threadPool) { threadPool = mQuotaClient->GetOrCreateThreadPool(); MOZ_ASSERT(threadPool); } MOZ_ALWAYS_SUCCEEDS( threadPool->Dispatch(databaseMaintenance, NS_DISPATCH_NORMAL)); RegisterDatabaseMaintenance(databaseMaintenance); } } } mDirectoryInfos.Clear(); if (mDatabaseMaintenances.Count()) { mState = State::WaitingForDatabaseMaintenancesToComplete; } else { mState = State::Finishing; Finish(); } return NS_OK; } void Maintenance::Finish() { AssertIsOnBackgroundThread(); MOZ_ASSERT(mState == State::Finishing); if (NS_FAILED(mResultCode)) { nsCString errorName; GetErrorName(mResultCode, errorName); IDB_WARNING("Maintenance finished with error: %s", errorName.get()); } mDirectoryLock = nullptr; // It can happen that we are only referenced by mCurrentMaintenance which is // cleared in NoteFinishedMaintenance() RefPtr<Maintenance> kungFuDeathGrip = this; mQuotaClient->NoteFinishedMaintenance(this); mState = State::Complete; } NS_IMPL_ISUPPORTS_INHERITED0(Maintenance, Runnable) NS_IMETHODIMP Maintenance::Run() { MOZ_ASSERT(mState != State::Complete); nsresult rv; switch (mState) { case State::Initial: rv = Start(); break; case State::CreateIndexedDatabaseManager: rv = CreateIndexedDatabaseManager(); break; case State::IndexedDatabaseManagerOpen: rv = OpenDirectory(); break; case State::DirectoryWorkOpen: rv = DirectoryWork(); break; case State::BeginDatabaseMaintenance: rv = BeginDatabaseMaintenance(); break; case State::Finishing: Finish(); return NS_OK; default: MOZ_CRASH("Bad state!"); } if (NS_WARN_IF(NS_FAILED(rv)) && 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)); } } return NS_OK; } void Maintenance::DirectoryLockAcquired(DirectoryLock* aLock) { AssertIsOnBackgroundThread(); MOZ_ASSERT(mState == State::DirectoryOpenPending); MOZ_ASSERT(!mDirectoryLock); mDirectoryLock = aLock; 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); if (NS_SUCCEEDED(mResultCode)) { mResultCode = NS_ERROR_FAILURE; } mState = State::Finishing; Finish(); } void DatabaseMaintenance::PerformMaintenanceOnDatabase() { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(mMaintenance); MOZ_ASSERT(mMaintenance->StartTime()); MOZ_ASSERT(!mDatabasePath.IsEmpty()); MOZ_ASSERT(!mGroup.IsEmpty()); MOZ_ASSERT(!mOrigin.IsEmpty()); class MOZ_STACK_CLASS AutoClose final { nsCOMPtr<mozIStorageConnection> mConnection; public: explicit AutoClose(mozIStorageConnection* aConnection) : mConnection(aConnection) { MOZ_ASSERT(aConnection); } ~AutoClose() { MOZ_ASSERT(mConnection); MOZ_ALWAYS_SUCCEEDS(mConnection->Close()); } }; if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || mMaintenance->IsAborted()) { return; } nsCOMPtr<nsIFile> databaseFile = GetFileForPath(mDatabasePath); MOZ_ASSERT(databaseFile); nsCOMPtr<mozIStorageConnection> connection; nsresult rv = GetStorageConnection(databaseFile, mPersistenceType, mGroup, mOrigin, TelemetryIdForFile(databaseFile), getter_AddRefs(connection)); if (NS_WARN_IF(NS_FAILED(rv))) { return; } AutoClose autoClose(connection); AutoProgressHandler progressHandler(mMaintenance); if (NS_WARN_IF(NS_FAILED(progressHandler.Register(connection)))) { return; } bool databaseIsOk; rv = CheckIntegrity(connection, &databaseIsOk); if (NS_WARN_IF(NS_FAILED(rv))) { return; } if (NS_WARN_IF(!databaseIsOk)) { // XXX Handle this somehow! Probably need to clear all storage for the // origin. Needs followup. MOZ_ASSERT(false, "Database corruption detected!"); return; } MaintenanceAction maintenanceAction; rv = DetermineMaintenanceAction(connection, databaseFile, &maintenanceAction); if (NS_WARN_IF(NS_FAILED(rv))) { return; } 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(aConnection); MOZ_ASSERT(aOk); if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || mMaintenance->IsAborted()) { return NS_ERROR_ABORT; } nsresult rv; // First do a full integrity_check. Scope statements tightly here because // later operations require zero live statements. { nsCOMPtr<mozIStorageStatement> stmt; rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( "PRAGMA integrity_check(1);" ), getter_AddRefs(stmt)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } bool hasResult; rv = stmt->ExecuteStep(&hasResult); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(hasResult); nsString result; rv = stmt->GetString(0, result); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (NS_WARN_IF(!result.EqualsLiteral("ok"))) { *aOk = false; return NS_OK; } } // Now enable and check for foreign key constraints. { int32_t foreignKeysWereEnabled; { nsCOMPtr<mozIStorageStatement> stmt; rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( "PRAGMA foreign_keys;" ), getter_AddRefs(stmt)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } bool hasResult; rv = stmt->ExecuteStep(&hasResult); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(hasResult); rv = stmt->GetInt32(0, &foreignKeysWereEnabled); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } if (!foreignKeysWereEnabled) { rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "PRAGMA foreign_keys = ON;")); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } bool foreignKeyError; { nsCOMPtr<mozIStorageStatement> stmt; rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( "PRAGMA foreign_key_check;" ), getter_AddRefs(stmt)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->ExecuteStep(&foreignKeyError); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } if (!foreignKeysWereEnabled) { nsAutoCString stmtSQL; stmtSQL.AssignLiteral("PRAGMA foreign_keys = "); stmtSQL.AppendLiteral("OFF"); stmtSQL.Append(';'); rv = aConnection->ExecuteSimpleSQL(stmtSQL); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } 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(aConnection); MOZ_ASSERT(aDatabaseFile); MOZ_ASSERT(aMaintenanceAction); if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || mMaintenance->IsAborted()) { return NS_ERROR_ABORT; } int32_t schemaVersion; nsresult rv = aConnection->GetSchemaVersion(&schemaVersion); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // 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; } bool lowDiskSpace = IndexedDatabaseManager::InLowDiskSpaceMode(); if (QuotaManager::IsRunningXPCShellTests()) { // If we're running XPCShell then we want to test both the low disk space // and normal disk space code paths so pick semi-randomly based on the // current time. lowDiskSpace = ((PR_Now() / PR_USEC_PER_MSEC) % 2) == 0; } // If we're low on disk space then the best we can hope for is that an // incremental vacuum might free some space. That is a journaled operation so // it may not be possible even then. if (lowDiskSpace) { *aMaintenanceAction = MaintenanceAction::IncrementalVacuum; 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); // Check to see when we last vacuumed this database. nsCOMPtr<mozIStorageStatement> stmt; rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( "SELECT last_vacuum_time, last_vacuum_size " "FROM database;" ), getter_AddRefs(stmt)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } bool hasResult; rv = stmt->ExecuteStep(&hasResult); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(hasResult); PRTime lastVacuumTime; rv = stmt->GetInt64(0, &lastVacuumTime); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } int64_t lastVacuumSize; rv = stmt->GetInt64(1, &lastVacuumSize); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } 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."); 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. rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE VIRTUAL TABLE __stats__ USING dbstat;" "CREATE TEMP TABLE __temp_stats__ AS SELECT * FROM __stats__;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Calculate the percentage of the database pages that are not in contiguous // order. rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( "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;" ), getter_AddRefs(stmt)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->ExecuteStep(&hasResult); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(hasResult); int32_t percentUnordered; rv = stmt->GetInt32(0, &percentUnordered); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } 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%. int64_t currentFileSize; rv = aDatabaseFile->GetFileSize(¤tFileSize); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } 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. rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( "PRAGMA freelist_count;" ), getter_AddRefs(stmt)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->ExecuteStep(&hasResult); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(hasResult); int32_t freelistCount; rv = stmt->GetInt32(0, &freelistCount); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } 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. rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( "SELECT SUM(unused) * 100.0 / SUM(pgsize) " "FROM __temp_stats__;" ), getter_AddRefs(stmt)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->ExecuteStep(&hasResult); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(hasResult); int32_t percentUnused; rv = stmt->GetInt32(0, &percentUnused); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } 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()); MOZ_ASSERT(aConnection); if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || mMaintenance->IsAborted()) { return; } nsresult rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "PRAGMA incremental_vacuum;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return; } } void DatabaseMaintenance::FullVacuum(mozIStorageConnection* aConnection, nsIFile* aDatabaseFile) { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(aConnection); MOZ_ASSERT(aDatabaseFile); if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || mMaintenance->IsAborted()) { return; } nsresult rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "VACUUM;" )); if (NS_WARN_IF(NS_FAILED(rv))) { return; } PRTime vacuumTime = PR_Now(); MOZ_ASSERT(vacuumTime > 0); int64_t fileSize; rv = aDatabaseFile->GetFileSize(&fileSize); if (NS_WARN_IF(NS_FAILED(rv))) { return; } MOZ_ASSERT(fileSize > 0); nsCOMPtr<mozIStorageStatement> stmt; rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( "UPDATE database " "SET last_vacuum_time = :time" ", last_vacuum_size = :size;" ), getter_AddRefs(stmt)); if (NS_WARN_IF(NS_FAILED(rv))) { return; } rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("time"), vacuumTime); if (NS_WARN_IF(NS_FAILED(rv))) { return; } rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("size"), fileSize); if (NS_WARN_IF(NS_FAILED(rv))) { return; } rv = stmt->Execute(); if (NS_WARN_IF(NS_FAILED(rv))) { return; } } void DatabaseMaintenance::RunOnOwningThread() { AssertIsOnBackgroundThread(); 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; } nsresult DatabaseMaintenance:: AutoProgressHandler::Register(mozIStorageConnection* aConnection) { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(aConnection); // We want to quickly bail out of any operation if the user becomes active, so // use a small granularity here since database performance isn't critical. static const int32_t kProgressGranularity = 50; nsCOMPtr<mozIStorageProgressHandler> oldHandler; nsresult rv = aConnection->SetProgressHandler(kProgressGranularity, this, getter_AddRefs(oldHandler)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(!oldHandler); mConnection = aConnection; return NS_OK; } void DatabaseMaintenance:: AutoProgressHandler::Unregister() { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(mConnection); nsCOMPtr<mozIStorageProgressHandler> oldHandler; nsresult rv = mConnection->RemoveProgressHandler(getter_AddRefs(oldHandler)); Unused << NS_WARN_IF(NS_FAILED(rv)); MOZ_ASSERT_IF(NS_SUCCEEDED(rv), oldHandler == this); } NS_IMETHODIMP_(MozExternalRefCountType) DatabaseMaintenance:: AutoProgressHandler::AddRef() { NS_ASSERT_OWNINGTHREAD(DatabaseMaintenance::AutoProgressHandler); #ifdef DEBUG mDEBUGRefCnt++; #endif return 2; } NS_IMETHODIMP_(MozExternalRefCountType) DatabaseMaintenance:: AutoProgressHandler::Release() { NS_ASSERT_OWNINGTHREAD(DatabaseMaintenance::AutoProgressHandler); #ifdef DEBUG mDEBUGRefCnt--; #endif return 1; } NS_IMPL_QUERY_INTERFACE(DatabaseMaintenance::AutoProgressHandler, mozIStorageProgressHandler) NS_IMETHODIMP DatabaseMaintenance:: AutoProgressHandler::OnProgress(mozIStorageConnection* aConnection, bool* _retval) { NS_ASSERT_OWNINGTHREAD(DatabaseMaintenance::AutoProgressHandler); MOZ_ASSERT(aConnection); MOZ_ASSERT(mConnection == aConnection); MOZ_ASSERT(_retval); *_retval = mMaintenance->IsAborted(); return NS_OK; } /******************************************************************************* * Local class implementations ******************************************************************************/ NS_IMPL_ISUPPORTS(CompressDataBlobsFunction, mozIStorageFunction) NS_IMPL_ISUPPORTS(EncodeKeysFunction, mozIStorageFunction) #if !defined(MOZ_B2G) nsresult UpgradeFileIdsFunction::Init(nsIFile* aFMDirectory, mozIStorageConnection* aConnection) { // This file manager doesn't need real origin info, etc. The only purpose is // to store file ids without adding more complexity or code duplication. RefPtr<FileManager> fileManager = new FileManager(PERSISTENCE_TYPE_INVALID, EmptyCString(), EmptyCString(), false, EmptyString(), false); nsresult rv = fileManager->Init(aFMDirectory, aConnection); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsAutoPtr<NormalJSContext> context(NormalJSContext::Create()); if (NS_WARN_IF(!context)) { return NS_ERROR_FAILURE; } mFileManager.swap(fileManager); mContext = context; return NS_OK; } NS_IMPL_ISUPPORTS(UpgradeFileIdsFunction, mozIStorageFunction) NS_IMETHODIMP UpgradeFileIdsFunction::OnFunctionCall(mozIStorageValueArray* aArguments, nsIVariant** aResult) { MOZ_ASSERT(aArguments); MOZ_ASSERT(aResult); MOZ_ASSERT(mFileManager); MOZ_ASSERT(mContext); PROFILER_LABEL("IndexedDB", "UpgradeFileIdsFunction::OnFunctionCall", js::ProfileEntry::Category::STORAGE); uint32_t argc; nsresult rv = aArguments->GetNumEntries(&argc); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (argc != 2) { NS_WARNING("Don't call me with the wrong number of arguments!"); return NS_ERROR_UNEXPECTED; } StructuredCloneReadInfo cloneInfo; DatabaseOperationBase::GetStructuredCloneReadInfoFromValueArray(aArguments, 1, 0, mFileManager, &cloneInfo); JSContext* cx = mContext->Context(); JSAutoRequest ar(cx); JSAutoCompartment ac(cx, mContext->Global()); JS::Rooted<JS::Value> clone(cx); if (NS_WARN_IF(!IDBObjectStore::DeserializeUpgradeValue(cx, cloneInfo, &clone))) { return NS_ERROR_DOM_DATA_CLONE_ERR; } nsAutoString fileIds; for (uint32_t count = cloneInfo.mFiles.Length(), index = 0; index < count; index++) { StructuredCloneFile& file = cloneInfo.mFiles[index]; MOZ_ASSERT(file.mFileInfo); const int64_t id = file.mFileInfo->Id(); if (index) { fileIds.Append(' '); } fileIds.AppendInt(file.mType == StructuredCloneFile::eBlob ? id : -id); } nsCOMPtr<nsIVariant> result = new mozilla::storage::TextVariant(fileIds); result.forget(aResult); return NS_OK; } #endif // MOZ_B2G // static void DatabaseOperationBase::GetBindingClauseForKeyRange( const SerializedKeyRange& aKeyRange, const nsACString& aKeyColumnName, nsAutoCString& aBindingClause) { MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(!aKeyColumnName.IsEmpty()); NS_NAMED_LITERAL_CSTRING(andStr, " AND "); NS_NAMED_LITERAL_CSTRING(spacecolon, " :"); NS_NAMED_LITERAL_CSTRING(lowerKey, "lower_key"); if (aKeyRange.isOnly()) { // Both keys equal. aBindingClause = andStr + aKeyColumnName + NS_LITERAL_CSTRING(" =") + spacecolon + lowerKey; return; } aBindingClause.Truncate(); if (!aKeyRange.lower().IsUnset()) { // Lower key is set. aBindingClause.Append(andStr + aKeyColumnName); aBindingClause.AppendLiteral(" >"); if (!aKeyRange.lowerOpen()) { aBindingClause.AppendLiteral("="); } aBindingClause.Append(spacecolon + lowerKey); } if (!aKeyRange.upper().IsUnset()) { // Upper key is set. aBindingClause.Append(andStr + aKeyColumnName); aBindingClause.AppendLiteral(" <"); if (!aKeyRange.upperOpen()) { aBindingClause.AppendLiteral("="); } aBindingClause.Append(spacecolon + NS_LITERAL_CSTRING("upper_key")); } MOZ_ASSERT(!aBindingClause.IsEmpty()); } // static uint64_t DatabaseOperationBase::ReinterpretDoubleAsUInt64(double aDouble) { // This is a duplicate of the js engine's byte munging in StructuredClone.cpp return BitwiseCast<uint64_t>(aDouble); } // static template <typename T> nsresult DatabaseOperationBase::GetStructuredCloneReadInfoFromSource( T* aSource, uint32_t aDataIndex, uint32_t aFileIdsIndex, FileManager* aFileManager, StructuredCloneReadInfo* aInfo) { MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(aSource); MOZ_ASSERT(aFileManager); MOZ_ASSERT(aInfo); int32_t columnType; nsresult rv = aSource->GetTypeOfIndex(aDataIndex, &columnType); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(columnType == mozIStorageStatement::VALUE_TYPE_BLOB || columnType == mozIStorageStatement::VALUE_TYPE_INTEGER); bool isNull; rv = aSource->GetIsNull(aFileIdsIndex, &isNull); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsString fileIds; if (isNull) { fileIds.SetIsVoid(true); } else { rv = aSource->GetString(aFileIdsIndex, fileIds); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } if (columnType == mozIStorageStatement::VALUE_TYPE_INTEGER) { uint64_t intData; rv = aSource->GetInt64(aDataIndex, reinterpret_cast<int64_t*>(&intData)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = GetStructuredCloneReadInfoFromExternalBlob(intData, aFileManager, fileIds, aInfo); } else { const uint8_t* blobData; uint32_t blobDataLength; nsresult rv = aSource->GetSharedBlob(aDataIndex, &blobDataLength, &blobData); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = GetStructuredCloneReadInfoFromBlob(blobData, blobDataLength, aFileManager, fileIds, aInfo); } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } // static nsresult DatabaseOperationBase::GetStructuredCloneReadInfoFromBlob( const uint8_t* aBlobData, uint32_t aBlobDataLength, FileManager* aFileManager, const nsAString& aFileIds, StructuredCloneReadInfo* aInfo) { MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(aFileManager); MOZ_ASSERT(aInfo); PROFILER_LABEL("IndexedDB", "DatabaseOperationBase::GetStructuredCloneReadInfoFromBlob", js::ProfileEntry::Category::STORAGE); const char* compressed = reinterpret_cast<const char*>(aBlobData); size_t compressedLength = size_t(aBlobDataLength); size_t uncompressedLength; if (NS_WARN_IF(!snappy::GetUncompressedLength(compressed, compressedLength, &uncompressedLength))) { return NS_ERROR_FILE_CORRUPTED; } AutoTArray<uint8_t, 512> uncompressed; if (NS_WARN_IF(!uncompressed.SetLength(uncompressedLength, fallible))) { return NS_ERROR_OUT_OF_MEMORY; } char* uncompressedBuffer = reinterpret_cast<char*>(uncompressed.Elements()); if (NS_WARN_IF(!snappy::RawUncompress(compressed, compressedLength, uncompressedBuffer))) { return NS_ERROR_FILE_CORRUPTED; } if (!aInfo->mData.WriteBytes(uncompressedBuffer, uncompressed.Length())) { return NS_ERROR_OUT_OF_MEMORY; } if (!aFileIds.IsVoid()) { nsresult rv = DeserializeStructuredCloneFiles(aFileManager, aFileIds, aInfo->mFiles, &aInfo->mHasPreprocessInfo); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } return NS_OK; } // static nsresult DatabaseOperationBase::GetStructuredCloneReadInfoFromExternalBlob( uint64_t aIntData, FileManager* aFileManager, const nsAString& aFileIds, StructuredCloneReadInfo* aInfo) { MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(aFileManager); MOZ_ASSERT(aInfo); PROFILER_LABEL( "IndexedDB", "DatabaseOperationBase::GetStructuredCloneReadInfoFromExternalBlob", js::ProfileEntry::Category::STORAGE); nsresult rv; if (!aFileIds.IsVoid()) { rv = DeserializeStructuredCloneFiles(aFileManager, aFileIds, aInfo->mFiles, &aInfo->mHasPreprocessInfo); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } // Higher and lower 32 bits described // in ObjectStoreAddOrPutRequestOp::DoDatabaseWork. uint32_t index = uint32_t(aIntData & 0xFFFFFFFF); if (index >= aInfo->mFiles.Length()) { MOZ_ASSERT(false, "Bad index value!"); return NS_ERROR_UNEXPECTED; } StructuredCloneFile& file = aInfo->mFiles[index]; MOZ_ASSERT(file.mFileInfo); MOZ_ASSERT(file.mType == StructuredCloneFile::eStructuredClone); nsCOMPtr<nsIFile> nativeFile = GetFileForFileInfo(file.mFileInfo); if (NS_WARN_IF(!nativeFile)) { return NS_ERROR_FAILURE; } nsCOMPtr<nsIInputStream> fileInputStream; rv = NS_NewLocalFileInputStream(getter_AddRefs(fileInputStream), nativeFile); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } RefPtr<SnappyUncompressInputStream> snappyInputStream = new SnappyUncompressInputStream(fileInputStream); do { char buffer[kFileCopyBufferSize]; uint32_t numRead; rv = snappyInputStream->Read(buffer, sizeof(buffer), &numRead); if (NS_WARN_IF(NS_FAILED(rv))) { break; } if (!numRead) { break; } if (NS_WARN_IF(!aInfo->mData.WriteBytes(buffer, numRead))) { rv = NS_ERROR_OUT_OF_MEMORY; break; } } while (true); return rv; } // static nsresult DatabaseOperationBase::BindKeyRangeToStatement( const SerializedKeyRange& aKeyRange, mozIStorageStatement* aStatement) { MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(aStatement); nsresult rv = NS_OK; if (!aKeyRange.lower().IsUnset()) { rv = aKeyRange.lower().BindToStatement(aStatement, NS_LITERAL_CSTRING("lower_key")); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } if (aKeyRange.isOnly()) { return rv; } if (!aKeyRange.upper().IsUnset()) { rv = aKeyRange.upper().BindToStatement(aStatement, NS_LITERAL_CSTRING("upper_key")); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } return NS_OK; } // static nsresult DatabaseOperationBase::BindKeyRangeToStatement( const SerializedKeyRange& aKeyRange, mozIStorageStatement* aStatement, const nsCString& aLocale) { #ifndef ENABLE_INTL_API return BindKeyRangeToStatement(aKeyRange, aStatement); #else MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(aStatement); MOZ_ASSERT(!aLocale.IsEmpty()); nsresult rv = NS_OK; if (!aKeyRange.lower().IsUnset()) { Key lower; rv = aKeyRange.lower().ToLocaleBasedKey(lower, aLocale); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = lower.BindToStatement(aStatement, NS_LITERAL_CSTRING("lower_key")); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } if (aKeyRange.isOnly()) { return rv; } if (!aKeyRange.upper().IsUnset()) { Key upper; rv = aKeyRange.upper().ToLocaleBasedKey(upper, aLocale); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = upper.BindToStatement(aStatement, NS_LITERAL_CSTRING("upper_key")); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } return NS_OK; #endif } // static void DatabaseOperationBase::AppendConditionClause(const nsACString& aColumnName, const nsACString& aArgName, bool aLessThan, bool aEquals, nsAutoCString& aResult) { aResult += NS_LITERAL_CSTRING(" AND ") + aColumnName + NS_LITERAL_CSTRING(" "); if (aLessThan) { aResult.Append('<'); } else { aResult.Append('>'); } if (aEquals) { aResult.Append('='); } aResult += NS_LITERAL_CSTRING(" :") + aArgName; } // static nsresult DatabaseOperationBase::GetUniqueIndexTableForObjectStore( TransactionBase* aTransaction, int64_t aObjectStoreId, Maybe<UniqueIndexTable>& aMaybeUniqueIndexTable) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aTransaction); MOZ_ASSERT(aObjectStoreId); MOZ_ASSERT(aMaybeUniqueIndexTable.isNothing()); const RefPtr<FullObjectStoreMetadata> objectStoreMetadata = aTransaction->GetMetadataForObjectStoreId(aObjectStoreId); MOZ_ASSERT(objectStoreMetadata); if (!objectStoreMetadata->mIndexes.Count()) { return NS_OK; } const uint32_t indexCount = objectStoreMetadata->mIndexes.Count(); MOZ_ASSERT(indexCount > 0); aMaybeUniqueIndexTable.emplace(); UniqueIndexTable* uniqueIndexTable = aMaybeUniqueIndexTable.ptr(); MOZ_ASSERT(uniqueIndexTable); for (auto iter = objectStoreMetadata->mIndexes.Iter(); !iter.Done(); iter.Next()) { FullIndexMetadata* value = iter.UserData(); MOZ_ASSERT(!uniqueIndexTable->Get(value->mCommonMetadata.id())); if (NS_WARN_IF(!uniqueIndexTable->Put(value->mCommonMetadata.id(), value->mCommonMetadata.unique(), fallible))) { break; } } if (NS_WARN_IF(aMaybeUniqueIndexTable.ref().Count() != indexCount)) { IDB_REPORT_INTERNAL_ERR(); aMaybeUniqueIndexTable.reset(); NS_WARNING("out of memory"); return NS_ERROR_OUT_OF_MEMORY; } #ifdef DEBUG aMaybeUniqueIndexTable.ref().MarkImmutable(); #endif return NS_OK; } // static nsresult DatabaseOperationBase::IndexDataValuesFromUpdateInfos( const nsTArray<IndexUpdateInfo>& aUpdateInfos, const UniqueIndexTable& aUniqueIndexTable, nsTArray<IndexDataValue>& aIndexValues) { MOZ_ASSERT(aIndexValues.IsEmpty()); MOZ_ASSERT_IF(!aUpdateInfos.IsEmpty(), aUniqueIndexTable.Count()); PROFILER_LABEL("IndexedDB", "DatabaseOperationBase::IndexDataValuesFromUpdateInfos", js::ProfileEntry::Category::STORAGE); const uint32_t count = aUpdateInfos.Length(); if (!count) { return NS_OK; } if (NS_WARN_IF(!aIndexValues.SetCapacity(count, fallible))) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_OUT_OF_MEMORY; } for (uint32_t idxIndex = 0; idxIndex < count; idxIndex++) { const IndexUpdateInfo& updateInfo = aUpdateInfos[idxIndex]; const int64_t& indexId = updateInfo.indexId(); const Key& key = updateInfo.value(); const Key& sortKey = updateInfo.localizedValue(); bool unique = false; MOZ_ALWAYS_TRUE(aUniqueIndexTable.Get(indexId, &unique)); IndexDataValue idv(indexId, unique, key, sortKey); MOZ_ALWAYS_TRUE( aIndexValues.InsertElementSorted(idv, fallible)); } return NS_OK; } // static nsresult DatabaseOperationBase::InsertIndexTableRows( DatabaseConnection* aConnection, const int64_t aObjectStoreId, const Key& aObjectStoreKey, const FallibleTArray<IndexDataValue>& aIndexValues) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(!aObjectStoreKey.IsUnset()); PROFILER_LABEL("IndexedDB", "DatabaseOperationBase::InsertIndexTableRows", js::ProfileEntry::Category::STORAGE); const uint32_t count = aIndexValues.Length(); if (!count) { return NS_OK; } NS_NAMED_LITERAL_CSTRING(objectStoreIdString, "object_store_id"); NS_NAMED_LITERAL_CSTRING(objectDataKeyString, "object_data_key"); NS_NAMED_LITERAL_CSTRING(indexIdString, "index_id"); NS_NAMED_LITERAL_CSTRING(valueString, "value"); NS_NAMED_LITERAL_CSTRING(valueLocaleString, "value_locale"); DatabaseConnection::CachedStatement insertUniqueStmt; DatabaseConnection::CachedStatement insertStmt; nsresult rv; for (uint32_t index = 0; index < count; index++) { const IndexDataValue& info = aIndexValues[index]; DatabaseConnection::CachedStatement& stmt = info.mUnique ? insertUniqueStmt : insertStmt; if (stmt) { stmt.Reset(); } else if (info.mUnique) { rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( "INSERT INTO unique_index_data " "(index_id, value, object_store_id, object_data_key, value_locale) " "VALUES (:index_id, :value, :object_store_id, :object_data_key, :value_locale);"), &stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } else { rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( "INSERT OR IGNORE INTO index_data " "(index_id, value, object_data_key, object_store_id, value_locale) " "VALUES (:index_id, :value, :object_data_key, :object_store_id, :value_locale);"), &stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } rv = stmt->BindInt64ByName(indexIdString, info.mIndexId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = info.mKey.BindToStatement(stmt, valueString); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = info.mSortKey.BindToStatement(stmt, valueLocaleString); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->BindInt64ByName(objectStoreIdString, aObjectStoreId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aObjectStoreKey.BindToStatement(stmt, objectDataKeyString); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->Execute(); 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.mKey == aIndexValues[index2].mKey) { // 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. rv = NS_OK; break; } } } if (NS_FAILED(rv)) { return rv; } } return NS_OK; } // static nsresult DatabaseOperationBase::DeleteIndexDataTableRows( DatabaseConnection* aConnection, const Key& aObjectStoreKey, const FallibleTArray<IndexDataValue>& aIndexValues) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(!aObjectStoreKey.IsUnset()); PROFILER_LABEL("IndexedDB", "DatabaseOperationBase::DeleteIndexDataTableRows", js::ProfileEntry::Category::STORAGE); const uint32_t count = aIndexValues.Length(); if (!count) { return NS_OK; } NS_NAMED_LITERAL_CSTRING(indexIdString, "index_id"); NS_NAMED_LITERAL_CSTRING(valueString, "value"); NS_NAMED_LITERAL_CSTRING(objectDataKeyString, "object_data_key"); DatabaseConnection::CachedStatement deleteUniqueStmt; DatabaseConnection::CachedStatement deleteStmt; nsresult rv; for (uint32_t index = 0; index < count; index++) { const IndexDataValue& indexValue = aIndexValues[index]; DatabaseConnection::CachedStatement& stmt = indexValue.mUnique ? deleteUniqueStmt : deleteStmt; if (stmt) { stmt.Reset(); } else if (indexValue.mUnique) { rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( "DELETE FROM unique_index_data " "WHERE index_id = :index_id " "AND value = :value;"), &stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } else { rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( "DELETE FROM index_data " "WHERE index_id = :index_id " "AND value = :value " "AND object_data_key = :object_data_key;"), &stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } rv = stmt->BindInt64ByName(indexIdString, indexValue.mIndexId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = indexValue.mKey.BindToStatement(stmt, valueString); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (!indexValue.mUnique) { rv = aObjectStoreKey.BindToStatement(stmt, objectDataKeyString); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } rv = stmt->Execute(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } return NS_OK; } // static nsresult DatabaseOperationBase::DeleteObjectStoreDataTableRowsWithIndexes( DatabaseConnection* aConnection, const int64_t aObjectStoreId, const OptionalKeyRange& aKeyRange) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(aObjectStoreId); #ifdef DEBUG { bool hasIndexes = false; MOZ_ASSERT(NS_SUCCEEDED( ObjectStoreHasIndexes(aConnection, aObjectStoreId, &hasIndexes))); MOZ_ASSERT(hasIndexes, "Don't use this slow method if there are no indexes!"); } #endif PROFILER_LABEL("IndexedDB", "DatabaseOperationBase::" "DeleteObjectStoreDataTableRowsWithIndexes", js::ProfileEntry::Category::STORAGE); const bool singleRowOnly = aKeyRange.type() == OptionalKeyRange::TSerializedKeyRange && aKeyRange.get_SerializedKeyRange().isOnly(); NS_NAMED_LITERAL_CSTRING(objectStoreIdString, "object_store_id"); NS_NAMED_LITERAL_CSTRING(keyString, "key"); nsresult rv; Key objectStoreKey; DatabaseConnection::CachedStatement selectStmt; if (singleRowOnly) { rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( "SELECT index_data_values " "FROM object_data " "WHERE object_store_id = :object_store_id " "AND key = :key;"), &selectStmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } objectStoreKey = aKeyRange.get_SerializedKeyRange().lower(); rv = objectStoreKey.BindToStatement(selectStmt, keyString); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } else { nsAutoCString keyRangeClause; if (aKeyRange.type() == OptionalKeyRange::TSerializedKeyRange) { GetBindingClauseForKeyRange(aKeyRange.get_SerializedKeyRange(), keyString, keyRangeClause); } rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( "SELECT index_data_values, key " "FROM object_data " "WHERE object_store_id = :") + objectStoreIdString + keyRangeClause + NS_LITERAL_CSTRING(";"), &selectStmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (aKeyRange.type() == OptionalKeyRange::TSerializedKeyRange) { rv = BindKeyRangeToStatement(aKeyRange, selectStmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } } rv = selectStmt->BindInt64ByName(objectStoreIdString, aObjectStoreId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } DatabaseConnection::CachedStatement deleteStmt; AutoTArray<IndexDataValue, 32> indexValues; DebugOnly<uint32_t> resultCountDEBUG = 0; bool hasResult; while (NS_SUCCEEDED(rv = selectStmt->ExecuteStep(&hasResult)) && hasResult) { if (!singleRowOnly) { rv = objectStoreKey.SetFromStatement(selectStmt, 1); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } indexValues.ClearAndRetainStorage(); } rv = ReadCompressedIndexDataValues(selectStmt, 0, indexValues); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = DeleteIndexDataTableRows(aConnection, objectStoreKey, indexValues); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (deleteStmt) { MOZ_ALWAYS_SUCCEEDS(deleteStmt->Reset()); } else { rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( "DELETE FROM object_data " "WHERE object_store_id = :object_store_id " "AND key = :key;"), &deleteStmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } rv = deleteStmt->BindInt64ByName(objectStoreIdString, aObjectStoreId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = objectStoreKey.BindToStatement(deleteStmt, keyString); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = deleteStmt->Execute(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } resultCountDEBUG++; } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT_IF(singleRowOnly, resultCountDEBUG <= 1); return NS_OK; } // static nsresult DatabaseOperationBase::UpdateIndexValues( DatabaseConnection* aConnection, const int64_t aObjectStoreId, const Key& aObjectStoreKey, const FallibleTArray<IndexDataValue>& aIndexValues) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(!aObjectStoreKey.IsUnset()); PROFILER_LABEL("IndexedDB", "DatabaunseOperationBase::UpdateIndexValues", js::ProfileEntry::Category::STORAGE); UniqueFreePtr<uint8_t> indexDataValues; uint32_t indexDataValuesLength; nsresult rv = MakeCompressedIndexDataValues(aIndexValues, indexDataValues, &indexDataValuesLength); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(!indexDataValuesLength == !(indexDataValues.get())); DatabaseConnection::CachedStatement updateStmt; rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( "UPDATE object_data " "SET index_data_values = :index_data_values " "WHERE object_store_id = :object_store_id " "AND key = :key;"), &updateStmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } NS_NAMED_LITERAL_CSTRING(indexDataValuesString, "index_data_values"); if (indexDataValues) { rv = updateStmt->BindAdoptedBlobByName(indexDataValuesString, indexDataValues.release(), indexDataValuesLength); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } else { rv = updateStmt->BindNullByName(indexDataValuesString); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } rv = updateStmt->BindInt64ByName(NS_LITERAL_CSTRING("object_store_id"), aObjectStoreId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aObjectStoreKey.BindToStatement(updateStmt, NS_LITERAL_CSTRING("key")); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = updateStmt->Execute(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } // static nsresult DatabaseOperationBase::ObjectStoreHasIndexes(DatabaseConnection* aConnection, const int64_t aObjectStoreId, bool* aHasIndexes) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(aObjectStoreId); MOZ_ASSERT(aHasIndexes); DatabaseConnection::CachedStatement stmt; nsresult rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( "SELECT id " "FROM object_store_index " "WHERE object_store_id = :object_store_id " "LIMIT 1;"), &stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("object_store_id"), aObjectStoreId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } bool hasResult; rv = stmt->ExecuteStep(&hasResult); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } *aHasIndexes = hasResult; return NS_OK; } NS_IMPL_ISUPPORTS_INHERITED(DatabaseOperationBase, Runnable, mozIStorageProgressHandler) NS_IMETHODIMP DatabaseOperationBase::OnProgress(mozIStorageConnection* aConnection, bool* _retval) { MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(aConnection); MOZ_ASSERT(_retval); // This is intentionally racy. *_retval = !OperationMayProceed(); return NS_OK; } DatabaseOperationBase:: AutoSetProgressHandler::AutoSetProgressHandler() : mConnection(nullptr) #ifdef DEBUG , mDEBUGDatabaseOp(nullptr) #endif { MOZ_ASSERT(!IsOnBackgroundThread()); } DatabaseOperationBase:: AutoSetProgressHandler::~AutoSetProgressHandler() { MOZ_ASSERT(!IsOnBackgroundThread()); if (mConnection) { nsCOMPtr<mozIStorageProgressHandler> oldHandler; MOZ_ALWAYS_SUCCEEDS( mConnection->RemoveProgressHandler(getter_AddRefs(oldHandler))); MOZ_ASSERT(oldHandler == mDEBUGDatabaseOp); } } nsresult DatabaseOperationBase:: AutoSetProgressHandler::Register(mozIStorageConnection* aConnection, DatabaseOperationBase* aDatabaseOp) { MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(aConnection); MOZ_ASSERT(aDatabaseOp); MOZ_ASSERT(!mConnection); nsCOMPtr<mozIStorageProgressHandler> oldProgressHandler; nsresult rv = aConnection->SetProgressHandler(kStorageProgressGranularity, aDatabaseOp, getter_AddRefs(oldProgressHandler)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(!oldProgressHandler); mConnection = aConnection; #ifdef DEBUG mDEBUGDatabaseOp = aDatabaseOp; #endif return NS_OK; } MutableFile::MutableFile(nsIFile* aFile, Database* aDatabase, FileInfo* aFileInfo) : BackgroundMutableFileParentBase(FILE_HANDLE_STORAGE_IDB, aDatabase->Id(), IntString(aFileInfo->Id()), aFile) , mDatabase(aDatabase) , mFileInfo(aFileInfo) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aDatabase); MOZ_ASSERT(aFileInfo); } MutableFile::~MutableFile() { mDatabase->UnregisterMutableFile(this); } already_AddRefed<MutableFile> MutableFile::Create(nsIFile* aFile, Database* aDatabase, FileInfo* aFileInfo) { AssertIsOnBackgroundThread(); RefPtr<MutableFile> newMutableFile = new MutableFile(aFile, aDatabase, aFileInfo); if (!aDatabase->RegisterMutableFile(newMutableFile)) { return nullptr; } return newMutableFile.forget(); } void MutableFile::NoteActiveState() { AssertIsOnBackgroundThread(); mDatabase->NoteActiveMutableFile(); } void MutableFile::NoteInactiveState() { AssertIsOnBackgroundThread(); mDatabase->NoteInactiveMutableFile(); } PBackgroundParent* MutableFile::GetBackgroundParent() const { AssertIsOnBackgroundThread(); MOZ_ASSERT(!IsActorDestroyed()); return GetDatabase()->GetBackgroundParent(); } already_AddRefed<nsISupports> MutableFile::CreateStream(bool aReadOnly) { AssertIsOnBackgroundThread(); PersistenceType persistenceType = mDatabase->Type(); const nsACString& group = mDatabase->Group(); const nsACString& origin = mDatabase->Origin(); nsCOMPtr<nsISupports> result; if (aReadOnly) { RefPtr<FileInputStream> stream = FileInputStream::Create(persistenceType, group, origin, mFile, -1, -1, nsIFileInputStream::DEFER_OPEN); result = NS_ISUPPORTS_CAST(nsIFileInputStream*, stream); } else { RefPtr<FileStream> stream = FileStream::Create(persistenceType, group, origin, mFile, -1, -1, nsIFileStream::DEFER_OPEN); result = NS_ISUPPORTS_CAST(nsIFileStream*, stream); } if (NS_WARN_IF(!result)) { return nullptr; } return result.forget(); } already_AddRefed<BlobImpl> MutableFile::CreateBlobImpl() { AssertIsOnBackgroundThread(); RefPtr<BlobImpl> blobImpl = new BlobImplStoredFile(mFile, mFileInfo, /* aSnapshot */ true); return blobImpl.forget(); } PBackgroundFileHandleParent* MutableFile::AllocPBackgroundFileHandleParent(const FileMode& aMode) { AssertIsOnBackgroundThread(); // Once a database is closed it must not try to open new file handles. if (NS_WARN_IF(mDatabase->IsClosed())) { if (!mDatabase->IsInvalidated()) { ASSERT_UNLESS_FUZZING(); } return nullptr; } if (!gFileHandleThreadPool) { RefPtr<FileHandleThreadPool> fileHandleThreadPool = FileHandleThreadPool::Create(); if (NS_WARN_IF(!fileHandleThreadPool)) { return nullptr; } gFileHandleThreadPool = fileHandleThreadPool; } return BackgroundMutableFileParentBase::AllocPBackgroundFileHandleParent( aMode); } bool MutableFile::RecvPBackgroundFileHandleConstructor( PBackgroundFileHandleParent* aActor, const FileMode& aMode) { AssertIsOnBackgroundThread(); MOZ_ASSERT(!mDatabase->IsClosed()); if (NS_WARN_IF(mDatabase->IsInvalidated())) { // This is an expected race. We don't want the child to die here, just don't // actually do any work. return true; } return BackgroundMutableFileParentBase::RecvPBackgroundFileHandleConstructor( aActor, aMode); } bool MutableFile::RecvGetFileId(int64_t* aFileId) { AssertIsOnBackgroundThread(); MOZ_ASSERT(mFileInfo); if (NS_WARN_IF(!IndexedDatabaseManager::InTestingMode())) { ASSERT_UNLESS_FUZZING(); return false; } *aFileId = mFileInfo->Id(); return true; } FactoryOp::FactoryOp(Factory* aFactory, already_AddRefed<ContentParent> aContentParent, const CommonFactoryRequestParams& aCommonParams, bool aDeleting) : DatabaseOperationBase(aFactory->GetLoggingInfo()->Id(), aFactory->GetLoggingInfo()->NextRequestSN()) , mFactory(aFactory) , mContentParent(Move(aContentParent)) , mCommonParams(aCommonParams) , mState(State::Initial) , mIsApp(false) , mEnforcingQuota(true) , mDeleting(aDeleting) , mBlockedDatabaseOpen(false) , mChromeWriteAccessAllowed(false) , mFileHandleDisabled(false) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aFactory); MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); } nsresult FactoryOp::Open() { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mState == State::Initial); // Swap this to the stack now to ensure that we release it on this thread. RefPtr<ContentParent> contentParent; mContentParent.swap(contentParent); if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || !OperationMayProceed()) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } PermissionRequestBase::PermissionValue permission; nsresult rv = CheckPermission(contentParent, &permission); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(permission == PermissionRequestBase::kPermissionAllowed || permission == PermissionRequestBase::kPermissionDenied || permission == PermissionRequestBase::kPermissionPrompt); if (permission == PermissionRequestBase::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<mozIStorageService> ss; if (NS_WARN_IF(!(ss = do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID)))) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } } const DatabaseMetadata& metadata = mCommonParams.metadata(); QuotaManager::GetStorageId(metadata.persistenceType(), mOrigin, Client::IDB, mDatabaseId); mDatabaseId.Append('*'); mDatabaseId.Append(NS_ConvertUTF16toUTF8(metadata.name())); if (permission == PermissionRequestBase::kPermissionPrompt) { mState = State::PermissionChallenge; MOZ_ALWAYS_SUCCEEDS(mOwningThread->Dispatch(this, NS_DISPATCH_NORMAL)); return NS_OK; } MOZ_ASSERT(permission == PermissionRequestBase::kPermissionAllowed); mState = State::FinishOpen; MOZ_ALWAYS_SUCCEEDS(mOwningThread->Dispatch(this, NS_DISPATCH_NORMAL)); return NS_OK; } nsresult FactoryOp::ChallengePermission() { AssertIsOnOwningThread(); MOZ_ASSERT(mState == State::PermissionChallenge); const PrincipalInfo& principalInfo = mCommonParams.principalInfo(); MOZ_ASSERT(principalInfo.type() == PrincipalInfo::TContentPrincipalInfo); if (NS_WARN_IF(!SendPermissionChallenge(principalInfo))) { return NS_ERROR_FAILURE; } return NS_OK; } nsresult FactoryOp::RetryCheckPermission() { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mState == State::PermissionRetry); MOZ_ASSERT(mCommonParams.principalInfo().type() == PrincipalInfo::TContentPrincipalInfo); // Swap this to the stack now to ensure that we release it on this thread. RefPtr<ContentParent> contentParent; mContentParent.swap(contentParent); if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || !OperationMayProceed()) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } PermissionRequestBase::PermissionValue permission; nsresult rv = CheckPermission(contentParent, &permission); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(permission == PermissionRequestBase::kPermissionAllowed || permission == PermissionRequestBase::kPermissionDenied || permission == PermissionRequestBase::kPermissionPrompt); if (permission == PermissionRequestBase::kPermissionDenied || permission == PermissionRequestBase::kPermissionPrompt) { return NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR; } MOZ_ASSERT(permission == PermissionRequestBase::kPermissionAllowed); mState = State::FinishOpen; MOZ_ALWAYS_SUCCEEDS(mOwningThread->Dispatch(this, NS_DISPATCH_NORMAL)); return NS_OK; } nsresult FactoryOp::DirectoryOpen() { AssertIsOnOwningThread(); MOZ_ASSERT(mState == State::DirectoryOpenPending); MOZ_ASSERT(mDirectoryLock); MOZ_ASSERT(!mDatabaseFilePath.IsEmpty()); // gFactoryOps could be null here if the child process crashed or something // and that cleaned up the last Factory actor. if (!gFactoryOps) { return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } // See if this FactoryOp needs to wait. bool delayed = false; for (uint32_t index = gFactoryOps->Length(); index > 0; index--) { RefPtr<FactoryOp>& existingOp = (*gFactoryOps)[index - 1]; if (MustWaitFor(*existingOp)) { // Only one op can be delayed. MOZ_ASSERT(!existingOp->mDelayedOp); existingOp->mDelayedOp = this; delayed = true; break; } } // Adding this to the factory ops list will block any additional ops from // proceeding until this one is done. gFactoryOps->AppendElement(this); if (!delayed) { QuotaClient* quotaClient = QuotaClient::GetInstance(); MOZ_ASSERT(quotaClient); if (RefPtr<Maintenance> currentMaintenance = quotaClient->GetCurrentMaintenance()) { if (RefPtr<DatabaseMaintenance> databaseMaintenance = currentMaintenance->GetDatabaseMaintenance(mDatabaseFilePath)) { databaseMaintenance->WaitForCompletion(this); delayed = true; } } } mBlockedDatabaseOpen = true; // Balanced in FinishSendResults(). IncreaseBusyCount(); mState = State::DatabaseOpenPending; if (!delayed) { nsresult rv = DatabaseOpen(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } 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* quotaManager = QuotaManager::Get(); MOZ_ASSERT(quotaManager); // Must set this before dispatching otherwise we will race with the IO thread. mState = State::DatabaseWorkOpen; nsresult rv = quotaManager->IOThread()->Dispatch(this, 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 FactoryOp::WaitForTransactions() { AssertIsOnOwningThread(); MOZ_ASSERT(mState == State::BeginVersionChange || mState == State::WaitingForOtherDatabasesToClose); MOZ_ASSERT(!mDatabaseId.IsEmpty()); MOZ_ASSERT(!IsActorDestroyed()); mState = State::WaitingForTransactionsToComplete; RefPtr<WaitForTransactionsHelper> helper = new WaitForTransactionsHelper(mDatabaseId, this); helper->WaitForTransactions(); } void FactoryOp::FinishSendResults() { AssertIsOnOwningThread(); MOZ_ASSERT(mState == State::SendingResults); MOZ_ASSERT(mFactory); // Make sure to release the factory on this thread. RefPtr<Factory> factory; mFactory.swap(factory); if (mBlockedDatabaseOpen) { if (mDelayedOp) { MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(mDelayedOp.forget())); } MOZ_ASSERT(gFactoryOps); gFactoryOps->RemoveElement(this); // Match the IncreaseBusyCount in DirectoryOpen(). DecreaseBusyCount(); } mState = State::Completed; } nsresult FactoryOp::CheckPermission(ContentParent* aContentParent, PermissionRequestBase::PermissionValue* aPermission) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mState == State::Initial || mState == State::PermissionRetry); const PrincipalInfo& principalInfo = mCommonParams.principalInfo(); if (principalInfo.type() != PrincipalInfo::TSystemPrincipalInfo) { if (principalInfo.type() != PrincipalInfo::TContentPrincipalInfo) { if (aContentParent) { // We just want ContentPrincipalInfo or SystemPrincipalInfo. aContentParent->KillHard("IndexedDB CheckPermission 0"); } return NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR; } if (NS_WARN_IF(!Preferences::GetBool(kPrefIndexedDBEnabled, false))) { if (aContentParent) { // The DOM in the other process should have kept us from receiving any // indexedDB messages so assume that the child is misbehaving. aContentParent->KillHard("IndexedDB CheckPermission 1"); } return NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR; } const ContentPrincipalInfo& contentPrincipalInfo = principalInfo.get_ContentPrincipalInfo(); if (contentPrincipalInfo.attrs().mPrivateBrowsingId != 0) { // IndexedDB is currently disabled in privateBrowsing. return NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR; } } mFileHandleDisabled = !Preferences::GetBool(kPrefFileHandleEnabled); PersistenceType persistenceType = mCommonParams.metadata().persistenceType(); MOZ_ASSERT(principalInfo.type() != PrincipalInfo::TNullPrincipalInfo); if (principalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) { MOZ_ASSERT(mState == State::Initial); MOZ_ASSERT(persistenceType == PERSISTENCE_TYPE_PERSISTENT); if (aContentParent) { // Check to make sure that the child process has access to the database it // is accessing. NS_NAMED_LITERAL_CSTRING(permissionStringBase, PERMISSION_STRING_CHROME_BASE); NS_ConvertUTF16toUTF8 databaseName(mCommonParams.metadata().name()); NS_NAMED_LITERAL_CSTRING(readSuffix, PERMISSION_STRING_CHROME_READ_SUFFIX); NS_NAMED_LITERAL_CSTRING(writeSuffix, PERMISSION_STRING_CHROME_WRITE_SUFFIX); const nsAutoCString permissionStringWrite = permissionStringBase + databaseName + writeSuffix; const nsAutoCString permissionStringRead = permissionStringBase + databaseName + readSuffix; bool canWrite = CheckAtLeastOneAppHasPermission(aContentParent, permissionStringWrite); bool canRead; if (canWrite) { MOZ_ASSERT(CheckAtLeastOneAppHasPermission(aContentParent, permissionStringRead)); canRead = true; } else { canRead = CheckAtLeastOneAppHasPermission(aContentParent, permissionStringRead); } // Deleting a database requires write permissions. if (mDeleting && !canWrite) { aContentParent->KillHard("IndexedDB CheckPermission 2"); IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } // Opening or deleting requires read permissions. if (!canRead) { aContentParent->KillHard("IndexedDB CheckPermission 3"); IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } mChromeWriteAccessAllowed = canWrite; } else { mChromeWriteAccessAllowed = true; } if (State::Initial == mState) { QuotaManager::GetInfoForChrome(&mSuffix, &mGroup, &mOrigin, &mIsApp); MOZ_ASSERT(!QuotaManager::IsFirstPromptRequired(persistenceType, mOrigin, mIsApp)); mEnforcingQuota = QuotaManager::IsQuotaEnforced(persistenceType, mOrigin, mIsApp); } *aPermission = PermissionRequestBase::kPermissionAllowed; return NS_OK; } MOZ_ASSERT(principalInfo.type() == PrincipalInfo::TContentPrincipalInfo); nsresult rv; nsCOMPtr<nsIPrincipal> principal = PrincipalInfoToPrincipal(principalInfo, &rv); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsCString suffix; nsCString group; nsCString origin; bool isApp; rv = QuotaManager::GetInfoFromPrincipal(principal, &suffix, &group, &origin, &isApp); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } #ifdef IDB_MOBILE if (persistenceType == PERSISTENCE_TYPE_PERSISTENT && !QuotaManager::IsOriginInternal(origin) && !isApp) { return NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR; } #endif PermissionRequestBase::PermissionValue permission; if (QuotaManager::IsFirstPromptRequired(persistenceType, origin, isApp)) { rv = PermissionRequestBase::GetCurrentPermission(principal, &permission); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } else { permission = PermissionRequestBase::kPermissionAllowed; } if (permission != PermissionRequestBase::kPermissionDenied && State::Initial == mState) { mSuffix = suffix; mGroup = group; mOrigin = origin; mIsApp = isApp; mEnforcingQuota = QuotaManager::IsQuotaEnforced(persistenceType, mOrigin, mIsApp); } *aPermission = permission; return NS_OK; } nsresult FactoryOp::SendVersionChangeMessages(DatabaseActorInfo* aDatabaseActorInfo, Database* aOpeningDatabase, uint64_t aOldVersion, const NullableVersion& 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) { FallibleTArray<MaybeBlockedDatabaseInfo> maybeBlockedDatabases; for (uint32_t index = 0; index < liveCount; index++) { Database* database = aDatabaseActorInfo->mLiveDatabases[index]; if ((!aOpeningDatabase || database != aOpeningDatabase) && !database->IsClosed() && NS_WARN_IF(!maybeBlockedDatabases.AppendElement(database, fallible))) { return NS_ERROR_OUT_OF_MEMORY; } } if (!maybeBlockedDatabases.IsEmpty()) { mMaybeBlockedDatabases.SwapElements(maybeBlockedDatabases); } } if (!mMaybeBlockedDatabases.IsEmpty()) { for (uint32_t count = mMaybeBlockedDatabases.Length(), index = 0; index < count; /* incremented conditionally */) { if (mMaybeBlockedDatabases[index]->SendVersionChange(aOldVersion, aNewVersion)) { index++; } else { // We don't want to wait forever if we were not able to send the // message. mMaybeBlockedDatabases.RemoveElementAt(index); count--; } } } return NS_OK; } // static bool FactoryOp::CheckAtLeastOneAppHasPermission(ContentParent* aContentParent, const nsACString& aPermissionString) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aContentParent); MOZ_ASSERT(!aPermissionString.IsEmpty()); return true; } nsresult FactoryOp::FinishOpen() { AssertIsOnOwningThread(); MOZ_ASSERT(mState == State::FinishOpen); MOZ_ASSERT(!mContentParent); if (QuotaManager::Get()) { nsresult rv = OpenDirectory(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } mState = State::QuotaManagerPending; QuotaManager::GetOrCreate(this); return NS_OK; } nsresult FactoryOp::QuotaManagerOpen() { AssertIsOnOwningThread(); MOZ_ASSERT(mState == State::QuotaManagerPending); if (NS_WARN_IF(!QuotaManager::Get())) { return NS_ERROR_FAILURE; } nsresult rv = OpenDirectory(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } nsresult FactoryOp::OpenDirectory() { AssertIsOnOwningThread(); MOZ_ASSERT(mState == State::FinishOpen || mState == State::QuotaManagerPending); MOZ_ASSERT(!mOrigin.IsEmpty()); MOZ_ASSERT(!mDirectoryLock); MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); MOZ_ASSERT(QuotaManager::Get()); // Need to get database file path in advance. const nsString& databaseName = mCommonParams.metadata().name(); PersistenceType persistenceType = mCommonParams.metadata().persistenceType(); QuotaManager* quotaManager = QuotaManager::Get(); MOZ_ASSERT(quotaManager); nsCOMPtr<nsIFile> dbFile; nsresult rv = quotaManager->GetDirectoryForOrigin(persistenceType, mOrigin, getter_AddRefs(dbFile)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = dbFile->Append(NS_LITERAL_STRING(IDB_DIRECTORY_NAME)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsAutoString filename; GetDatabaseFilename(databaseName, filename); rv = dbFile->Append(filename + NS_LITERAL_STRING(".sqlite")); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = dbFile->GetPath(mDatabaseFilePath); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } mState = State::DirectoryOpenPending; quotaManager->OpenDirectory(persistenceType, mGroup, mOrigin, mIsApp, Client::IDB, /* aExclusive */ false, this); 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.mOrigin == mOrigin && aExistingOp.mDatabaseId == mDatabaseId; } void FactoryOp::NoteDatabaseBlocked(Database* aDatabase) { AssertIsOnOwningThread(); 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 (uint32_t count = mMaybeBlockedDatabases.Length(), index = 0; index < count; index++) { MaybeBlockedDatabaseInfo& info = mMaybeBlockedDatabases[index]; 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(); } } NS_IMPL_ISUPPORTS_INHERITED0(FactoryOp, DatabaseOperationBase) // 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() { nsresult rv; switch (mState) { case State::Initial: rv = Open(); break; case State::PermissionChallenge: rv = ChallengePermission(); break; case State::PermissionRetry: rv = RetryCheckPermission(); break; case State::FinishOpen: rv = FinishOpen(); break; case State::QuotaManagerPending: rv = QuotaManagerOpen(); break; case State::DatabaseOpenPending: rv = DatabaseOpen(); break; case State::DatabaseWorkOpen: rv = DoDatabaseWork(); break; case State::BeginVersionChange: rv = BeginVersionChange(); break; case State::WaitingForTransactionsToComplete: rv = DispatchToWorkThread(); break; case State::SendingResults: SendResults(); return NS_OK; default: MOZ_CRASH("Bad state!"); } if (NS_WARN_IF(NS_FAILED(rv)) && mState != State::SendingResults) { if (NS_SUCCEEDED(mResultCode)) { mResultCode = rv; } // Must set mState before dispatching otherwise we will race with the owning // thread. mState = State::SendingResults; if (IsOnOwningThread()) { SendResults(); } else { MOZ_ALWAYS_SUCCEEDS(mOwningThread->Dispatch(this, NS_DISPATCH_NORMAL)); } } return NS_OK; } void FactoryOp::DirectoryLockAcquired(DirectoryLock* aLock) { AssertIsOnOwningThread(); MOZ_ASSERT(mState == State::DirectoryOpenPending); MOZ_ASSERT(!mDirectoryLock); mDirectoryLock = aLock; nsresult rv = DirectoryOpen(); if (NS_WARN_IF(NS_FAILED(rv))) { if (NS_SUCCEEDED(mResultCode)) { mResultCode = 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()); return; } } void FactoryOp::DirectoryLockFailed() { AssertIsOnOwningThread(); MOZ_ASSERT(mState == State::DirectoryOpenPending); MOZ_ASSERT(!mDirectoryLock); if (NS_SUCCEEDED(mResultCode)) { IDB_REPORT_INTERNAL_ERR(); mResultCode = 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(); } bool FactoryOp::RecvPermissionRetry() { AssertIsOnOwningThread(); MOZ_ASSERT(!IsActorDestroyed()); MOZ_ASSERT(mState == State::PermissionChallenge); mContentParent = BackgroundParent::GetContentParent(Manager()->Manager()); mState = State::PermissionRetry; MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(this)); return true; } OpenDatabaseOp::OpenDatabaseOp(Factory* aFactory, already_AddRefed<ContentParent> aContentParent, const CommonFactoryRequestParams& aParams) : FactoryOp(aFactory, Move(aContentParent), aParams, /* aDeleting */ false) , mMetadata(new FullDatabaseMetadata(aParams.metadata())) , mRequestedVersion(aParams.metadata().version()) , mVersionChangeOp(nullptr) , mTelemetryId(0) { if (mContentParent) { // This is a little scary but it looks safe to call this off the main thread // for now. mOptionalContentParentId = Some(mContentParent->ChildID()); } } 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); PROFILER_LABEL("IndexedDB", "OpenDatabaseOp::DoDatabaseWork", js::ProfileEntry::Category::STORAGE); if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || !OperationMayProceed()) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } const nsString& databaseName = mCommonParams.metadata().name(); PersistenceType persistenceType = mCommonParams.metadata().persistenceType(); QuotaManager* quotaManager = QuotaManager::Get(); MOZ_ASSERT(quotaManager); nsCOMPtr<nsIFile> dbDirectory; nsresult rv = quotaManager->EnsureOriginIsInitialized(persistenceType, mSuffix, mGroup, mOrigin, mIsApp, getter_AddRefs(dbDirectory)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = dbDirectory->Append(NS_LITERAL_STRING(IDB_DIRECTORY_NAME)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } bool exists; rv = dbDirectory->Exists(&exists); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (!exists) { rv = dbDirectory->Create(nsIFile::DIRECTORY_TYPE, 0755); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } #ifdef DEBUG else { bool isDirectory; MOZ_ASSERT(NS_SUCCEEDED(dbDirectory->IsDirectory(&isDirectory))); MOZ_ASSERT(isDirectory); } #endif nsAutoString filename; GetDatabaseFilename(databaseName, filename); nsCOMPtr<nsIFile> dbFile; rv = dbDirectory->Clone(getter_AddRefs(dbFile)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = dbFile->Append(filename + NS_LITERAL_STRING(".sqlite")); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } mTelemetryId = TelemetryIdForFile(dbFile); #ifdef DEBUG nsString databaseFilePath; rv = dbFile->GetPath(databaseFilePath); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(databaseFilePath == mDatabaseFilePath); #endif nsCOMPtr<nsIFile> fmDirectory; rv = dbDirectory->Clone(getter_AddRefs(fmDirectory)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } const NS_ConvertASCIItoUTF16 filesSuffix(kFileManagerDirectoryNameSuffix); rv = fmDirectory->Append(filename + filesSuffix); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsCOMPtr<mozIStorageConnection> connection; rv = CreateStorageConnection(dbFile, fmDirectory, databaseName, persistenceType, mGroup, mOrigin, mTelemetryId, getter_AddRefs(connection)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } AutoSetProgressHandler asph; rv = asph.Register(connection, this); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = LoadDatabaseInformation(connection); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } 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. if (mMetadata->mCommonMetadata.version() == 0) { mRequestedVersion = 1; } else { // Otherwise, treat it as if the current version were requested. mRequestedVersion = mMetadata->mCommonMetadata.version(); } } if (NS_WARN_IF(mMetadata->mCommonMetadata.version() > mRequestedVersion)) { return NS_ERROR_DOM_INDEXEDDB_VERSION_ERR; } IndexedDatabaseManager* mgr = IndexedDatabaseManager::Get(); MOZ_ASSERT(mgr); RefPtr<FileManager> fileManager = mgr->GetFileManager(persistenceType, mOrigin, databaseName); if (!fileManager) { fileManager = new FileManager(persistenceType, mGroup, mOrigin, mIsApp, databaseName, mEnforcingQuota); rv = fileManager->Init(fmDirectory, connection); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } mgr->AddFileManager(fileManager); } mFileManager = fileManager.forget(); // Must set mState before dispatching otherwise we will race with the owning // thread. mState = (mMetadata->mCommonMetadata.version() == mRequestedVersion) ? State::SendingResults : State::BeginVersionChange; rv = mOwningThread->Dispatch(this, NS_DISPATCH_NORMAL); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } nsresult OpenDatabaseOp::LoadDatabaseInformation(mozIStorageConnection* aConnection) { AssertIsOnIOThread(); MOZ_ASSERT(aConnection); MOZ_ASSERT(mMetadata); // Load version information. nsCOMPtr<mozIStorageStatement> stmt; nsresult rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( "SELECT name, origin, version " "FROM database" ), getter_AddRefs(stmt)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } bool hasResult; rv = stmt->ExecuteStep(&hasResult); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (NS_WARN_IF(!hasResult)) { return NS_ERROR_FILE_CORRUPTED; } nsString databaseName; rv = stmt->GetString(0, databaseName); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (NS_WARN_IF(mCommonParams.metadata().name() != databaseName)) { return NS_ERROR_FILE_CORRUPTED; } nsCString origin; rv = stmt->GetUTF8String(1, origin); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (mOrigin != origin) { NS_WARNING("Origins don't match!"); } int64_t version; rv = stmt->GetInt64(2, &version); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } mMetadata->mCommonMetadata.version() = uint64_t(version); ObjectStoreTable& objectStores = mMetadata->mObjectStores; // Load object store names and ids. rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( "SELECT id, auto_increment, name, key_path " "FROM object_store" ), getter_AddRefs(stmt)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } Maybe<nsTHashtable<nsUint64HashKey>> usedIds; Maybe<nsTHashtable<nsStringHashKey>> usedNames; int64_t lastObjectStoreId = 0; while (NS_SUCCEEDED((rv = stmt->ExecuteStep(&hasResult))) && hasResult) { int64_t objectStoreId; rv = stmt->GetInt64(0, &objectStoreId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (!usedIds) { usedIds.emplace(); } if (NS_WARN_IF(objectStoreId <= 0) || NS_WARN_IF(usedIds.ref().Contains(objectStoreId))) { return NS_ERROR_FILE_CORRUPTED; } if (NS_WARN_IF(!usedIds.ref().PutEntry(objectStoreId, fallible))) { return NS_ERROR_OUT_OF_MEMORY; } nsString name; rv = stmt->GetString(2, name); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (!usedNames) { usedNames.emplace(); } if (NS_WARN_IF(usedNames.ref().Contains(name))) { return NS_ERROR_FILE_CORRUPTED; } if (NS_WARN_IF(!usedNames.ref().PutEntry(name, fallible))) { return NS_ERROR_OUT_OF_MEMORY; } RefPtr<FullObjectStoreMetadata> metadata = new FullObjectStoreMetadata(); metadata->mCommonMetadata.id() = objectStoreId; metadata->mCommonMetadata.name() = name; int32_t columnType; rv = stmt->GetTypeOfIndex(3, &columnType); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (columnType == mozIStorageStatement::VALUE_TYPE_NULL) { metadata->mCommonMetadata.keyPath() = KeyPath(0); } else { MOZ_ASSERT(columnType == mozIStorageStatement::VALUE_TYPE_TEXT); nsString keyPathSerialization; rv = stmt->GetString(3, keyPathSerialization); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } metadata->mCommonMetadata.keyPath() = KeyPath::DeserializeFromString(keyPathSerialization); if (NS_WARN_IF(!metadata->mCommonMetadata.keyPath().IsValid())) { return NS_ERROR_FILE_CORRUPTED; } } int64_t nextAutoIncrementId; rv = stmt->GetInt64(1, &nextAutoIncrementId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } metadata->mCommonMetadata.autoIncrement() = !!nextAutoIncrementId; metadata->mNextAutoIncrementId = nextAutoIncrementId; metadata->mCommittedAutoIncrementId = nextAutoIncrementId; if (NS_WARN_IF(!objectStores.Put(objectStoreId, metadata, fallible))) { return NS_ERROR_OUT_OF_MEMORY; } lastObjectStoreId = std::max(lastObjectStoreId, objectStoreId); } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } usedIds.reset(); usedNames.reset(); // Load index information rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( "SELECT " "id, object_store_id, name, key_path, unique_index, multientry, " "locale, is_auto_locale " "FROM object_store_index" ), getter_AddRefs(stmt)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } int64_t lastIndexId = 0; while (NS_SUCCEEDED((rv = stmt->ExecuteStep(&hasResult))) && hasResult) { int64_t objectStoreId; rv = stmt->GetInt64(1, &objectStoreId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } RefPtr<FullObjectStoreMetadata> objectStoreMetadata; if (NS_WARN_IF(!objectStores.Get(objectStoreId, getter_AddRefs(objectStoreMetadata)))) { return NS_ERROR_FILE_CORRUPTED; } MOZ_ASSERT(objectStoreMetadata->mCommonMetadata.id() == objectStoreId); int64_t indexId; rv = stmt->GetInt64(0, &indexId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (!usedIds) { usedIds.emplace(); } if (NS_WARN_IF(indexId <= 0) || NS_WARN_IF(usedIds.ref().Contains(indexId))) { return NS_ERROR_FILE_CORRUPTED; } if (NS_WARN_IF(!usedIds.ref().PutEntry(indexId, fallible))) { return NS_ERROR_OUT_OF_MEMORY; } nsString name; rv = stmt->GetString(2, name); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsAutoString hashName; hashName.AppendInt(indexId); hashName.Append(':'); hashName.Append(name); if (!usedNames) { usedNames.emplace(); } if (NS_WARN_IF(usedNames.ref().Contains(hashName))) { return NS_ERROR_FILE_CORRUPTED; } if (NS_WARN_IF(!usedNames.ref().PutEntry(hashName, fallible))) { return NS_ERROR_OUT_OF_MEMORY; } RefPtr<FullIndexMetadata> indexMetadata = new FullIndexMetadata(); indexMetadata->mCommonMetadata.id() = indexId; indexMetadata->mCommonMetadata.name() = name; #ifdef DEBUG { int32_t columnType; rv = stmt->GetTypeOfIndex(3, &columnType); MOZ_ASSERT(NS_SUCCEEDED(rv)); MOZ_ASSERT(columnType != mozIStorageStatement::VALUE_TYPE_NULL); } #endif nsString keyPathSerialization; rv = stmt->GetString(3, keyPathSerialization); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } indexMetadata->mCommonMetadata.keyPath() = KeyPath::DeserializeFromString(keyPathSerialization); if (NS_WARN_IF(!indexMetadata->mCommonMetadata.keyPath().IsValid())) { return NS_ERROR_FILE_CORRUPTED; } int32_t scratch; rv = stmt->GetInt32(4, &scratch); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } indexMetadata->mCommonMetadata.unique() = !!scratch; rv = stmt->GetInt32(5, &scratch); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } indexMetadata->mCommonMetadata.multiEntry() = !!scratch; #ifdef ENABLE_INTL_API const bool localeAware = !stmt->IsNull(6); if (localeAware) { rv = stmt->GetUTF8String(6, indexMetadata->mCommonMetadata.locale()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->GetInt32(7, &scratch); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } indexMetadata->mCommonMetadata.autoLocale() = !!scratch; // Update locale-aware indexes if necessary const nsCString& indexedLocale = indexMetadata->mCommonMetadata.locale(); const bool& isAutoLocale = indexMetadata->mCommonMetadata.autoLocale(); nsCString systemLocale = IndexedDatabaseManager::GetLocale(); if (!systemLocale.IsEmpty() && isAutoLocale && !indexedLocale.EqualsASCII(systemLocale.get())) { rv = UpdateLocaleAwareIndex(aConnection, indexMetadata->mCommonMetadata, systemLocale); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } } #endif if (NS_WARN_IF(!objectStoreMetadata->mIndexes.Put(indexId, indexMetadata, fallible))) { return NS_ERROR_OUT_OF_MEMORY; } lastIndexId = std::max(lastIndexId, indexId); } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (NS_WARN_IF(lastObjectStoreId == INT64_MAX) || NS_WARN_IF(lastIndexId == INT64_MAX)) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } mMetadata->mNextObjectStoreId = lastObjectStoreId + 1; mMetadata->mNextIndexId = lastIndexId + 1; return NS_OK; } #ifdef ENABLE_INTL_API /* static */ nsresult OpenDatabaseOp::UpdateLocaleAwareIndex(mozIStorageConnection* aConnection, const IndexMetadata& aIndexMetadata, const nsCString& aLocale) { nsresult rv; nsCString indexTable; if (aIndexMetadata.unique()) { indexTable.AssignLiteral("unique_index_data"); } else { indexTable.AssignLiteral("index_data"); } nsCString readQuery = NS_LITERAL_CSTRING("SELECT value, object_data_key FROM ") + indexTable + NS_LITERAL_CSTRING(" WHERE index_id = :index_id"); nsCOMPtr<mozIStorageStatement> readStmt; rv = aConnection->CreateStatement(readQuery, getter_AddRefs(readStmt)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = readStmt->BindInt64ByName(NS_LITERAL_CSTRING("index_id"), aIndexMetadata.id()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsCOMPtr<mozIStorageStatement> writeStmt; bool needCreateWriteQuery = true; bool hasResult; while (NS_SUCCEEDED((rv = readStmt->ExecuteStep(&hasResult))) && hasResult) { if (needCreateWriteQuery) { needCreateWriteQuery = false; nsCString writeQuery = NS_LITERAL_CSTRING("UPDATE ") + indexTable + NS_LITERAL_CSTRING("SET value_locale = :value_locale " "WHERE index_id = :index_id AND " "value = :value AND " "object_data_key = :object_data_key"); rv = aConnection->CreateStatement(writeQuery, getter_AddRefs(writeStmt)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } mozStorageStatementScoper scoper(writeStmt); rv = writeStmt->BindInt64ByName(NS_LITERAL_CSTRING("index_id"), aIndexMetadata.id()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } Key oldKey, newSortKey, objectKey; rv = oldKey.SetFromStatement(readStmt, 0); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = oldKey.BindToStatement(writeStmt, NS_LITERAL_CSTRING("value")); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = oldKey.ToLocaleBasedKey(newSortKey, aLocale); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = newSortKey.BindToStatement(writeStmt, NS_LITERAL_CSTRING("value_locale")); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = objectKey.SetFromStatement(readStmt, 1); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = objectKey.BindToStatement(writeStmt, NS_LITERAL_CSTRING("object_data_key")); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = writeStmt->Execute(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } nsCString metaQuery = NS_LITERAL_CSTRING("UPDATE object_store_index SET " "locale = :locale WHERE id = :id"); nsCOMPtr<mozIStorageStatement> metaStmt; rv = aConnection->CreateStatement(metaQuery, getter_AddRefs(metaStmt)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsString locale; locale.AssignWithConversion(aLocale); rv = metaStmt->BindStringByName(NS_LITERAL_CSTRING("locale"), locale); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = metaStmt->BindInt64ByName(NS_LITERAL_CSTRING("id"), aIndexMetadata.id()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = metaStmt->Execute(); return rv; } #endif 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(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } EnsureDatabaseActor(); if (mDatabase->IsInvalidated()) { IDB_REPORT_INTERNAL_ERR(); return 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)); MOZ_ASSERT(!info->mWaitingFactoryOp); MOZ_ASSERT(info->mMetadata == mMetadata); RefPtr<VersionChangeTransaction> transaction = new VersionChangeTransaction(this); if (NS_WARN_IF(!transaction->CopyDatabaseMetadata())) { return NS_ERROR_OUT_OF_MEMORY; } MOZ_ASSERT(info->mMetadata != mMetadata); mMetadata = info->mMetadata; NullableVersion newVersion = mRequestedVersion; nsresult rv = SendVersionChangeMessages(info, mDatabase, mMetadata->mCommonMetadata.version(), newVersion); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } mVersionChangeTransaction.swap(transaction); if (mMaybeBlockedDatabases.IsEmpty()) { // We don't need to wait on any databases, just jump to the transaction // pool. WaitForTransactions(); return NS_OK; } info->mWaitingFactoryOp = this; mState = State::WaitingForOtherDatabasesToClose; return NS_OK; } void OpenDatabaseOp::NoteDatabaseClosed(Database* aDatabase) { AssertIsOnOwningThread(); MOZ_ASSERT(aDatabase); MOZ_ASSERT(mState == State::WaitingForOtherDatabasesToClose || mState == State::WaitingForTransactionsToComplete || mState == State::DatabaseWorkVersionChange); if (mState != State::WaitingForOtherDatabasesToClose) { MOZ_ASSERT(mMaybeBlockedDatabases.IsEmpty()); MOZ_ASSERT(mRequestedVersion > aDatabase->Metadata()->mCommonMetadata.version(), "Must only be closing databases for a previous version!"); return; } MOZ_ASSERT(!mMaybeBlockedDatabases.IsEmpty()); bool actorDestroyed = IsActorDestroyed() || mDatabase->IsActorDestroyed(); nsresult rv; if (actorDestroyed) { IDB_REPORT_INTERNAL_ERR(); rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } else { rv = NS_OK; } // We are being called with an assuption that mWaitingFactoryOp holds a strong // reference to us. RefPtr<OpenDatabaseOp> kungFuDeathGrip; if (mMaybeBlockedDatabases.RemoveElement(aDatabase) && mMaybeBlockedDatabases.IsEmpty()) { if (actorDestroyed) { DatabaseActorInfo* info; MOZ_ALWAYS_TRUE(gLiveDatabaseHashtable->Get(mDatabaseId, &info)); MOZ_ASSERT(info->mWaitingFactoryOp == this); kungFuDeathGrip = static_cast<OpenDatabaseOp*>(info->mWaitingFactoryOp.get()); info->mWaitingFactoryOp = nullptr; } else { WaitForTransactions(); } } if (NS_WARN_IF(NS_FAILED(rv))) { if (NS_SUCCEEDED(mResultCode)) { mResultCode = rv; } // A strong reference is held in kungFuDeathGrip, so it's safe to call Run() // directly. mState = State::SendingResults; MOZ_ALWAYS_SUCCEEDS(Run()); } } 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::VERSION_CHANGE); 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<nsString> 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> versionChangeOp = new VersionChangeOp(this); uint64_t transactionId = versionChangeOp->StartOnConnectionPool( backgroundChildLoggingId, mVersionChangeTransaction->DatabaseId(), loggingSerialNumber, objectStoreNames, /* aIsWriteTransaction */ true); mVersionChangeOp = versionChangeOp; mVersionChangeTransaction->NoteActiveRequest(); mVersionChangeTransaction->SetActive(transactionId); return NS_OK; } nsresult OpenDatabaseOp::SendUpgradeNeeded() { AssertIsOnOwningThread(); MOZ_ASSERT(mState == State::DatabaseWorkVersionChange); MOZ_ASSERT(mVersionChangeTransaction); MOZ_ASSERT(mMaybeBlockedDatabases.IsEmpty()); MOZ_ASSERT(NS_SUCCEEDED(mResultCode)); MOZ_ASSERT_IF(!IsActorDestroyed(), mDatabase); if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || IsActorDestroyed()) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } RefPtr<VersionChangeTransaction> transaction; mVersionChangeTransaction.swap(transaction); nsresult rv = EnsureDatabaseActorIsAlive(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Transfer ownership to IPDL. transaction->SetActorAlive(); if (!mDatabase->SendPBackgroundIDBVersionChangeTransactionConstructor( transaction, 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_IF(NS_SUCCEEDED(mResultCode), mMaybeBlockedDatabases.IsEmpty()); MOZ_ASSERT_IF(NS_SUCCEEDED(mResultCode), !mVersionChangeTransaction); mMaybeBlockedDatabases.Clear(); DatabaseActorInfo* info; if (gLiveDatabaseHashtable && gLiveDatabaseHashtable->Get(mDatabaseId, &info) && info->mWaitingFactoryOp) { MOZ_ASSERT(info->mWaitingFactoryOp == this); // SendResults() should only be called by Run() and Run() should only be // called if there's a strong reference to the object that can't be cleared // here, so it's safe to clear mWaitingFactoryOp without adding additional // strong reference. info->mWaitingFactoryOp = nullptr; } if (mVersionChangeTransaction) { MOZ_ASSERT(NS_FAILED(mResultCode)); mVersionChangeTransaction->Abort(mResultCode, /* aForce */ true); mVersionChangeTransaction = nullptr; } if (IsActorDestroyed()) { if (NS_SUCCEEDED(mResultCode)) { mResultCode = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } } else { FactoryRequestResponse response; if (NS_SUCCEEDED(mResultCode)) { // 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. OpenDatabaseRequestResponse openResponse; openResponse.databaseParent() = mDatabase; response = openResponse; } else { response = ClampResultCode(rv); #ifdef DEBUG mResultCode = 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(mResultCode); } Unused << PBackgroundIDBFactoryRequestParent::Send__delete__(this, response); } if (mDatabase) { MOZ_ASSERT(!mDirectoryLock); if (NS_FAILED(mResultCode)) { mDatabase->Invalidate(); } // Make sure to release the database on this thread. mDatabase = nullptr; } else if (mDirectoryLock) { nsCOMPtr<nsIRunnable> callback = NewRunnableMethod(this, &OpenDatabaseOp::ConnectionClosedCallback); RefPtr<WaitForTransactionsHelper> helper = new WaitForTransactionsHelper(mDatabaseId, callback); helper->WaitForTransactions(); } FinishSendResults(); } void OpenDatabaseOp::ConnectionClosedCallback() { AssertIsOnOwningThread(); MOZ_ASSERT(NS_FAILED(mResultCode)); MOZ_ASSERT(mDirectoryLock); mDirectoryLock = nullptr; } void OpenDatabaseOp::EnsureDatabaseActor() { AssertIsOnOwningThread(); MOZ_ASSERT(mState == State::BeginVersionChange || mState == State::DatabaseWorkVersionChange || mState == State::SendingResults); MOZ_ASSERT(NS_SUCCEEDED(mResultCode)); 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; } auto factory = static_cast<Factory*>(Manager()); mDatabase = new Database(factory, mCommonParams.principalInfo(), mOptionalContentParentId, mGroup, mOrigin, mTelemetryId, mMetadata, mFileManager, mDirectoryLock.forget(), mFileHandleDisabled, mChromeWriteAccessAllowed); if (info) { info->mLiveDatabases.AppendElement(mDatabase); } else { info = new DatabaseActorInfo(mMetadata, mDatabase); gLiveDatabaseHashtable->Put(mDatabaseId, info); } // Balanced in Database::CleanupMetadata(). IncreaseBusyCount(); } nsresult OpenDatabaseOp::EnsureDatabaseActorIsAlive() { AssertIsOnOwningThread(); MOZ_ASSERT(mState == State::DatabaseWorkVersionChange || mState == State::SendingResults); MOZ_ASSERT(NS_SUCCEEDED(mResultCode)); MOZ_ASSERT(!IsActorDestroyed()); EnsureDatabaseActor(); if (mDatabase->IsActorAlive()) { return NS_OK; } auto factory = static_cast<Factory*>(Manager()); DatabaseSpec spec; MetadataToSpec(spec); // Transfer ownership to IPDL. mDatabase->SetActorAlive(); if (!factory->SendPBackgroundIDBDatabaseConstructor(mDatabase, spec, this)) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } return NS_OK; } void OpenDatabaseOp::MetadataToSpec(DatabaseSpec& aSpec) { AssertIsOnOwningThread(); MOZ_ASSERT(mMetadata); aSpec.metadata() = mMetadata->mCommonMetadata; for (auto objectStoreIter = mMetadata->mObjectStores.ConstIter(); !objectStoreIter.Done(); objectStoreIter.Next()) { FullObjectStoreMetadata* metadata = objectStoreIter.UserData(); MOZ_ASSERT(objectStoreIter.Key()); MOZ_ASSERT(metadata); // XXX This should really be fallible... ObjectStoreSpec* objectStoreSpec = aSpec.objectStores().AppendElement(); objectStoreSpec->metadata() = metadata->mCommonMetadata; for (auto indexIter = metadata->mIndexes.Iter(); !indexIter.Done(); indexIter.Next()) { FullIndexMetadata* indexMetadata = indexIter.UserData(); MOZ_ASSERT(indexIter.Key()); MOZ_ASSERT(indexMetadata); // XXX This should really be fallible... IndexMetadata* metadata = objectStoreSpec->indexes().AppendElement(); *metadata = indexMetadata->mCommonMetadata; } } } #ifdef DEBUG void OpenDatabaseOp::AssertMetadataConsistency(const FullDatabaseMetadata* aMetadata) { AssertIsOnBackgroundThread(); const FullDatabaseMetadata* thisDB = mMetadata; const FullDatabaseMetadata* otherDB = aMetadata; MOZ_ASSERT(thisDB); MOZ_ASSERT(otherDB); 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 (auto objectStoreIter = thisDB->mObjectStores.ConstIter(); !objectStoreIter.Done(); objectStoreIter.Next()) { FullObjectStoreMetadata* thisObjectStore = objectStoreIter.UserData(); MOZ_ASSERT(thisObjectStore); MOZ_ASSERT(!thisObjectStore->mDeleted); auto* otherObjectStore = MetadataNameOrIdMatcher<FullObjectStoreMetadata>::Match( otherDB->mObjectStores, thisObjectStore->mCommonMetadata.id()); MOZ_ASSERT(otherObjectStore); MOZ_ASSERT(thisObjectStore != otherObjectStore); 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. MOZ_ASSERT(thisObjectStore->mNextAutoIncrementId <= otherObjectStore->mNextAutoIncrementId); MOZ_ASSERT(thisObjectStore->mCommittedAutoIncrementId <= otherObjectStore->mCommittedAutoIncrementId || thisObjectStore->mCommittedAutoIncrementId == otherObjectStore->mNextAutoIncrementId); MOZ_ASSERT(!otherObjectStore->mDeleted); MOZ_ASSERT(thisObjectStore->mIndexes.Count() == otherObjectStore->mIndexes.Count()); for (auto indexIter = thisObjectStore->mIndexes.Iter(); !indexIter.Done(); indexIter.Next()) { FullIndexMetadata* thisIndex = indexIter.UserData(); MOZ_ASSERT(thisIndex); MOZ_ASSERT(!thisIndex->mDeleted); auto* otherIndex = MetadataNameOrIdMatcher<FullIndexMetadata>:: Match(otherObjectStore->mIndexes, thisIndex->mCommonMetadata.id()); MOZ_ASSERT(otherIndex); MOZ_ASSERT(thisIndex != otherIndex); 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; } PROFILER_LABEL("IndexedDB", "OpenDatabaseOp::VersionChangeOp::DoDatabaseWork", js::ProfileEntry::Category::STORAGE); IDB_LOG_MARK("IndexedDB %s: Parent Transaction[%lld]: " "Beginning database work", "IndexedDB %s: P T[%lld]: DB Start", IDB_LOG_ID_STRING(mBackgroundChildLoggingId), mLoggingSerialNumber); Transaction()->SetActiveOnConnectionThread(); nsresult rv = aConnection->BeginWriteTransaction(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } DatabaseConnection::CachedStatement updateStmt; rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( "UPDATE database " "SET version = :version;"), &updateStmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = updateStmt->BindInt64ByName(NS_LITERAL_CSTRING("version"), int64_t(mRequestedVersion)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = updateStmt->Execute(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } 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. NoteActorDestroyed(); #endif TransactionDatabaseOperationBase::Cleanup(); } void DeleteDatabaseOp::LoadPreviousVersion(nsIFile* aDatabaseFile) { AssertIsOnIOThread(); MOZ_ASSERT(aDatabaseFile); MOZ_ASSERT(mState == State::DatabaseWorkOpen); MOZ_ASSERT(!mPreviousVersion); PROFILER_LABEL("IndexedDB", "DeleteDatabaseOp::LoadPreviousVersion", js::ProfileEntry::Category::STORAGE); nsresult rv; nsCOMPtr<mozIStorageService> ss = do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID, &rv); if (NS_WARN_IF(NS_FAILED(rv))) { return; } nsCOMPtr<mozIStorageConnection> connection; rv = OpenDatabaseAndHandleBusy(ss, aDatabaseFile, getter_AddRefs(connection)); if (NS_WARN_IF(NS_FAILED(rv))) { return; } #ifdef DEBUG { nsCOMPtr<mozIStorageStatement> stmt; MOZ_ALWAYS_SUCCEEDS( connection->CreateStatement(NS_LITERAL_CSTRING( "SELECT name " "FROM database" ), getter_AddRefs(stmt))); bool hasResult; MOZ_ALWAYS_SUCCEEDS(stmt->ExecuteStep(&hasResult)); nsString databaseName; MOZ_ALWAYS_SUCCEEDS(stmt->GetString(0, databaseName)); MOZ_ASSERT(mCommonParams.metadata().name() == databaseName); } #endif nsCOMPtr<mozIStorageStatement> stmt; rv = connection->CreateStatement(NS_LITERAL_CSTRING( "SELECT version " "FROM database" ), getter_AddRefs(stmt)); if (NS_WARN_IF(NS_FAILED(rv))) { return; } bool hasResult; rv = stmt->ExecuteStep(&hasResult); if (NS_WARN_IF(NS_FAILED(rv))) { return; } if (NS_WARN_IF(!hasResult)) { return; } 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); // Swap this to the stack now to ensure that we release it on this thread. RefPtr<ContentParent> contentParent; mContentParent.swap(contentParent); nsresult rv = SendToIOThread(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } nsresult DeleteDatabaseOp::DoDatabaseWork() { AssertIsOnIOThread(); MOZ_ASSERT(mState == State::DatabaseWorkOpen); PROFILER_LABEL("IndexedDB", "DeleteDatabaseOp::DoDatabaseWork", js::ProfileEntry::Category::STORAGE); if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || !OperationMayProceed()) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } const nsString& databaseName = mCommonParams.metadata().name(); PersistenceType persistenceType = mCommonParams.metadata().persistenceType(); QuotaManager* quotaManager = QuotaManager::Get(); MOZ_ASSERT(quotaManager); nsCOMPtr<nsIFile> directory; nsresult rv = quotaManager->GetDirectoryForOrigin(persistenceType, mOrigin, getter_AddRefs(directory)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = directory->Append(NS_LITERAL_STRING(IDB_DIRECTORY_NAME)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = directory->GetPath(mDatabaseDirectoryPath); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsAutoString filename; GetDatabaseFilename(databaseName, filename); mDatabaseFilenameBase = filename; nsCOMPtr<nsIFile> dbFile; rv = directory->Clone(getter_AddRefs(dbFile)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = dbFile->Append(filename + NS_LITERAL_STRING(".sqlite")); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } #ifdef DEBUG nsString databaseFilePath; rv = dbFile->GetPath(databaseFilePath); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(databaseFilePath == mDatabaseFilePath); #endif bool exists; rv = dbFile->Exists(&exists); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } 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; } rv = mOwningThread->Dispatch(this, NS_DISPATCH_NORMAL); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } 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(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } DatabaseActorInfo* info; if (gLiveDatabaseHashtable->Get(mDatabaseId, &info)) { MOZ_ASSERT(!info->mWaitingFactoryOp); NullableVersion newVersion = null_t(); nsresult rv = SendVersionChangeMessages(info, nullptr, mPreviousVersion, newVersion); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (!mMaybeBlockedDatabases.IsEmpty()) { 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; } 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> versionChangeOp = new VersionChangeOp(this); QuotaManager* 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::NoteDatabaseClosed(Database* aDatabase) { AssertIsOnOwningThread(); MOZ_ASSERT(mState == State::WaitingForOtherDatabasesToClose); MOZ_ASSERT(!mMaybeBlockedDatabases.IsEmpty()); bool actorDestroyed = IsActorDestroyed(); nsresult rv; if (actorDestroyed) { IDB_REPORT_INTERNAL_ERR(); rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } else { rv = NS_OK; } // We are being called with an assuption that mWaitingFactoryOp holds a strong // reference to us. RefPtr<OpenDatabaseOp> kungFuDeathGrip; if (mMaybeBlockedDatabases.RemoveElement(aDatabase) && mMaybeBlockedDatabases.IsEmpty()) { if (actorDestroyed) { DatabaseActorInfo* info; MOZ_ALWAYS_TRUE(gLiveDatabaseHashtable->Get(mDatabaseId, &info)); MOZ_ASSERT(info->mWaitingFactoryOp == this); kungFuDeathGrip = static_cast<OpenDatabaseOp*>(info->mWaitingFactoryOp.get()); info->mWaitingFactoryOp = nullptr; } else { WaitForTransactions(); } } if (NS_WARN_IF(NS_FAILED(rv))) { if (NS_SUCCEEDED(mResultCode)) { mResultCode = rv; } // A strong reference is held in kungFuDeathGrip, so it's safe to call Run() // directly. mState = State::SendingResults; MOZ_ALWAYS_SUCCEEDS(Run()); } } void DeleteDatabaseOp::SendBlockedNotification() { AssertIsOnOwningThread(); MOZ_ASSERT(mState == State::WaitingForOtherDatabasesToClose); if (!IsActorDestroyed()) { Unused << SendBlocked(mPreviousVersion); } } void DeleteDatabaseOp::SendResults() { AssertIsOnOwningThread(); MOZ_ASSERT(mState == State::SendingResults); if (!IsActorDestroyed()) { FactoryRequestResponse response; if (NS_SUCCEEDED(mResultCode)) { response = DeleteDatabaseRequestResponse(mPreviousVersion); } else { response = ClampResultCode(mResultCode); } Unused << PBackgroundIDBFactoryRequestParent::Send__delete__(this, response); } mDirectoryLock = nullptr; FinishSendResults(); } nsresult DeleteDatabaseOp:: VersionChangeOp::DeleteFile(nsIFile* aDirectory, const nsAString& aFilename, QuotaManager* aQuotaManager) { AssertIsOnIOThread(); MOZ_ASSERT(aDirectory); MOZ_ASSERT(!aFilename.IsEmpty()); MOZ_ASSERT_IF(aQuotaManager, mDeleteDatabaseOp->mEnforcingQuota); MOZ_ASSERT(mDeleteDatabaseOp->mState == State::DatabaseWorkVersionChange); PROFILER_LABEL("IndexedDB", "DeleteDatabaseOp::VersionChangeOp::DeleteFile", js::ProfileEntry::Category::STORAGE); nsCOMPtr<nsIFile> file; nsresult rv = aDirectory->Clone(getter_AddRefs(file)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = file->Append(aFilename); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } int64_t fileSize; if (aQuotaManager) { rv = file->GetFileSize(&fileSize); if (rv == NS_ERROR_FILE_NOT_FOUND || rv == NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) { return NS_OK; } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(fileSize >= 0); } rv = file->Remove(false); if (rv == NS_ERROR_FILE_NOT_FOUND || rv == NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) { return NS_OK; } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (aQuotaManager && fileSize > 0) { const PersistenceType& persistenceType = mDeleteDatabaseOp->mCommonParams.metadata().persistenceType(); aQuotaManager->DecreaseUsageForOrigin(persistenceType, mDeleteDatabaseOp->mGroup, mDeleteDatabaseOp->mOrigin, fileSize); } return NS_OK; } nsresult DeleteDatabaseOp:: VersionChangeOp::RunOnIOThread() { AssertIsOnIOThread(); MOZ_ASSERT(mDeleteDatabaseOp->mState == State::DatabaseWorkVersionChange); PROFILER_LABEL("IndexedDB", "DeleteDatabaseOp::VersionChangeOp::RunOnIOThread", js::ProfileEntry::Category::STORAGE); 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<nsIFile> directory = GetFileForPath(mDeleteDatabaseOp->mDatabaseDirectoryPath); if (NS_WARN_IF(!directory)) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } // The database file counts towards quota. nsAutoString filename = mDeleteDatabaseOp->mDatabaseFilenameBase + NS_LITERAL_STRING(".sqlite"); nsresult rv = DeleteFile(directory, filename, quotaManager); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // .sqlite-journal files don't count towards quota. const NS_ConvertASCIItoUTF16 journalSuffix( kSQLiteJournalSuffix, LiteralStringLength(kSQLiteJournalSuffix)); filename = mDeleteDatabaseOp->mDatabaseFilenameBase + journalSuffix; rv = DeleteFile(directory, filename, /* doesn't count */ nullptr); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // .sqlite-shm files don't count towards quota. const NS_ConvertASCIItoUTF16 shmSuffix(kSQLiteSHMSuffix, LiteralStringLength(kSQLiteSHMSuffix)); filename = mDeleteDatabaseOp->mDatabaseFilenameBase + shmSuffix; rv = DeleteFile(directory, filename, /* doesn't count */ nullptr); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // .sqlite-wal files do count towards quota. const NS_ConvertASCIItoUTF16 walSuffix(kSQLiteWALSuffix, LiteralStringLength(kSQLiteWALSuffix)); filename = mDeleteDatabaseOp->mDatabaseFilenameBase + walSuffix; rv = DeleteFile(directory, filename, quotaManager); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsCOMPtr<nsIFile> fmDirectory; rv = directory->Clone(getter_AddRefs(fmDirectory)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // The files directory counts towards quota. const NS_ConvertASCIItoUTF16 filesSuffix( kFileManagerDirectoryNameSuffix, LiteralStringLength(kFileManagerDirectoryNameSuffix)); rv = fmDirectory->Append(mDeleteDatabaseOp->mDatabaseFilenameBase + filesSuffix); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } bool exists; rv = fmDirectory->Exists(&exists); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (exists) { bool isDirectory; rv = fmDirectory->IsDirectory(&isDirectory); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (NS_WARN_IF(!isDirectory)) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } uint64_t usage = 0; if (mDeleteDatabaseOp->mEnforcingQuota) { rv = FileManager::GetUsage(fmDirectory, &usage); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } rv = fmDirectory->Remove(true); if (NS_WARN_IF(NS_FAILED(rv))) { // We may have deleted some files, check if we can and update quota // information before returning the error. if (mDeleteDatabaseOp->mEnforcingQuota) { uint64_t newUsage; if (NS_SUCCEEDED(FileManager::GetUsage(fmDirectory, &newUsage))) { MOZ_ASSERT(newUsage <= usage); usage = usage - newUsage; } } } if (mDeleteDatabaseOp->mEnforcingQuota && usage) { quotaManager->DecreaseUsageForOrigin(persistenceType, mDeleteDatabaseOp->mGroup, mDeleteDatabaseOp->mOrigin, usage); } if (NS_FAILED(rv)) { return rv; } } IndexedDatabaseManager* mgr = IndexedDatabaseManager::Get(); MOZ_ASSERT(mgr); const nsString& databaseName = mDeleteDatabaseOp->mCommonParams.metadata().name(); mgr->InvalidateFileManager(persistenceType, mDeleteDatabaseOp->mOrigin, databaseName); rv = mOwningThread->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); RefPtr<DeleteDatabaseOp> deleteOp; mDeleteDatabaseOp.swap(deleteOp); if (deleteOp->IsActorDestroyed()) { IDB_REPORT_INTERNAL_ERR(); deleteOp->SetFailureCode(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); } else { DatabaseActorInfo* info; if (gLiveDatabaseHashtable->Get(deleteOp->mDatabaseId, &info) && info->mWaitingFactoryOp) { MOZ_ASSERT(info->mWaitingFactoryOp == deleteOp); info->mWaitingFactoryOp = nullptr; } if (NS_FAILED(mResultCode)) { if (NS_SUCCEEDED(deleteOp->ResultCode())) { deleteOp->SetFailureCode(mResultCode); } } else { // Inform all the other databases that they are now invalidated. That // should remove the previous metadata from our table. if (info) { MOZ_ASSERT(!info->mLiveDatabases.IsEmpty()); FallibleTArray<Database*> liveDatabases; if (NS_WARN_IF(!liveDatabases.AppendElements(info->mLiveDatabases, fallible))) { deleteOp->SetFailureCode(NS_ERROR_OUT_OF_MEMORY); } else { #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 (uint32_t count = liveDatabases.Length(), index = 0; index < count; index++) { RefPtr<Database> database = liveDatabases[index]; 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))) { if (NS_SUCCEEDED(mResultCode)) { mResultCode = rv; } MOZ_ALWAYS_SUCCEEDS(mOwningThread->Dispatch(this, NS_DISPATCH_NORMAL)); } return NS_OK; } TransactionDatabaseOperationBase::TransactionDatabaseOperationBase( TransactionBase* aTransaction) : DatabaseOperationBase(aTransaction->GetLoggingInfo()->Id(), aTransaction->GetLoggingInfo()->NextRequestSN()) , mTransaction(aTransaction) , mTransactionLoggingSerialNumber(aTransaction->LoggingSerialNumber()) , mInternalState(InternalState::Initial) , mTransactionIsAborted(aTransaction->IsAborted()) { MOZ_ASSERT(aTransaction); MOZ_ASSERT(LoggingSerialNumber()); } TransactionDatabaseOperationBase::TransactionDatabaseOperationBase( TransactionBase* aTransaction, uint64_t aLoggingSerialNumber) : DatabaseOperationBase(aTransaction->GetLoggingInfo()->Id(), aLoggingSerialNumber) , mTransaction(aTransaction) , mTransactionLoggingSerialNumber(aTransaction->LoggingSerialNumber()) , mInternalState(InternalState::Initial) , mTransactionIsAborted(aTransaction->IsAborted()) { MOZ_ASSERT(aTransaction); } TransactionDatabaseOperationBase::~TransactionDatabaseOperationBase() { MOZ_ASSERT(mInternalState == InternalState::Completed); MOZ_ASSERT(!mTransaction, "TransactionDatabaseOperationBase::Cleanup() was not called by a " "subclass!"); } #ifdef DEBUG void TransactionDatabaseOperationBase::AssertIsOnConnectionThread() const { MOZ_ASSERT(mTransaction); mTransaction->AssertIsOnConnectionThread(); } #endif // DEBUG uint64_t TransactionDatabaseOperationBase::StartOnConnectionPool( const nsID& aBackgroundChildLoggingId, const nsACString& aDatabaseId, int64_t aLoggingSerialNumber, const nsTArray<nsString>& 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(mTransaction); MOZ_ASSERT(NS_SUCCEEDED(mResultCode)); PROFILER_LABEL("IndexedDB", "TransactionDatabaseOperationBase::RunOnConnectionThread", js::ProfileEntry::Category::STORAGE); // 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. mResultCode = 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(); mResultCode = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } else { Database* database = mTransaction->GetDatabase(); MOZ_ASSERT(database); // Here we're actually going to perform the database operation. nsresult rv = database->EnsureConnection(); if (NS_WARN_IF(NS_FAILED(rv))) { mResultCode = rv; } else { DatabaseConnection* connection = database->GetConnection(); MOZ_ASSERT(connection); MOZ_ASSERT(connection->GetStorageConnection()); AutoSetProgressHandler autoProgress; if (mLoggingSerialNumber) { rv = autoProgress.Register(connection->GetStorageConnection(), this); if (NS_WARN_IF(NS_FAILED(rv))) { mResultCode = rv; } } if (NS_SUCCEEDED(rv)) { if (mLoggingSerialNumber) { IDB_LOG_MARK("IndexedDB %s: Parent Transaction[%lld] Request[%llu]: " "Beginning database work", "IndexedDB %s: P T[%lld] R[%llu]: DB Start", IDB_LOG_ID_STRING(mBackgroundChildLoggingId), mTransactionLoggingSerialNumber, mLoggingSerialNumber); } rv = DoDatabaseWork(connection); if (mLoggingSerialNumber) { IDB_LOG_MARK("IndexedDB %s: Parent Transaction[%lld] Request[%llu]: " "Finished database work", "IndexedDB %s: P T[%lld] R[%llu]: DB End", IDB_LOG_ID_STRING(mBackgroundChildLoggingId), mTransactionLoggingSerialNumber, mLoggingSerialNumber); } if (NS_FAILED(rv)) { mResultCode = 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(mOwningThread->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); 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<TransactionDatabaseOperationBase> 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); MOZ_ASSERT(mTransaction); if (NS_WARN_IF(IsActorDestroyed())) { // Don't send any notifications if the actor was destroyed already. if (NS_SUCCEEDED(mResultCode)) { IDB_REPORT_INTERNAL_ERR(); mResultCode = 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. mResultCode = NS_ERROR_DOM_INDEXEDDB_ABORT_ERR; } else if (NS_SUCCEEDED(mResultCode)) { if (aSendPreprocessInfo) { // This should not release the IPDL reference. mResultCode = SendPreprocessInfo(); } else { // This may release the IPDL reference. mResultCode = SendSuccessResult(); } } if (NS_FAILED(mResultCode)) { // This should definitely release the IPDL reference. if (!SendFailureResult(mResultCode)) { // Abort the transaction. mTransaction->Abort(mResultCode, /* aForce */ false); } } } if (aSendPreprocessInfo && NS_SUCCEEDED(mResultCode)) { mInternalState = InternalState::WaitingForContinue; } else { if (mLoggingSerialNumber) { mTransaction->NoteFinishedRequest(); } Cleanup(); mInternalState = InternalState::Completed; } } bool TransactionDatabaseOperationBase::Init(TransactionBase* aTransaction) { AssertIsOnBackgroundThread(); MOZ_ASSERT(mInternalState == InternalState::Initial); MOZ_ASSERT(aTransaction); return true; } void TransactionDatabaseOperationBase::Cleanup() { AssertIsOnOwningThread(); MOZ_ASSERT(mInternalState == InternalState::SendingResults); MOZ_ASSERT(mTransaction); mTransaction = nullptr; } 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(TransactionBase* aTransaction, nsresult aResultCode) : DatabaseOperationBase(aTransaction->GetLoggingInfo()->Id(), aTransaction->GetLoggingInfo()->NextRequestSN()) , mTransaction(aTransaction) , mResultCode(aResultCode) { MOZ_ASSERT(aTransaction); MOZ_ASSERT(LoggingSerialNumber()); } nsresult TransactionBase:: CommitOp::WriteAutoIncrementCounts() { MOZ_ASSERT(mTransaction); mTransaction->AssertIsOnConnectionThread(); MOZ_ASSERT(mTransaction->GetMode() == IDBTransaction::READ_WRITE || mTransaction->GetMode() == IDBTransaction::READ_WRITE_FLUSH || mTransaction->GetMode() == IDBTransaction::CLEANUP || mTransaction->GetMode() == IDBTransaction::VERSION_CHANGE); const nsTArray<RefPtr<FullObjectStoreMetadata>>& metadataArray = mTransaction->mModifiedAutoIncrementObjectStoreMetadataArray; if (!metadataArray.IsEmpty()) { NS_NAMED_LITERAL_CSTRING(osid, "osid"); NS_NAMED_LITERAL_CSTRING(ai, "ai"); Database* database = mTransaction->GetDatabase(); MOZ_ASSERT(database); DatabaseConnection* connection = database->GetConnection(); MOZ_ASSERT(connection); DatabaseConnection::CachedStatement stmt; nsresult rv; for (uint32_t count = metadataArray.Length(), index = 0; index < count; index++) { const RefPtr<FullObjectStoreMetadata>& metadata = metadataArray[index]; MOZ_ASSERT(!metadata->mDeleted); MOZ_ASSERT(metadata->mNextAutoIncrementId > 1); if (stmt) { MOZ_ALWAYS_SUCCEEDS(stmt->Reset()); } else { rv = connection->GetCachedStatement( NS_LITERAL_CSTRING("UPDATE object_store " "SET auto_increment = :") + ai + NS_LITERAL_CSTRING(" WHERE id = :") + osid + NS_LITERAL_CSTRING(";"), &stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } rv = stmt->BindInt64ByName(osid, metadata->mCommonMetadata.id()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->BindInt64ByName(ai, metadata->mNextAutoIncrementId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->Execute(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } } return NS_OK; } void TransactionBase:: CommitOp::CommitOrRollbackAutoIncrementCounts() { MOZ_ASSERT(mTransaction); mTransaction->AssertIsOnConnectionThread(); MOZ_ASSERT(mTransaction->GetMode() == IDBTransaction::READ_WRITE || mTransaction->GetMode() == IDBTransaction::READ_WRITE_FLUSH || mTransaction->GetMode() == IDBTransaction::CLEANUP || mTransaction->GetMode() == IDBTransaction::VERSION_CHANGE); nsTArray<RefPtr<FullObjectStoreMetadata>>& metadataArray = mTransaction->mModifiedAutoIncrementObjectStoreMetadataArray; if (!metadataArray.IsEmpty()) { bool committed = NS_SUCCEEDED(mResultCode); for (uint32_t count = metadataArray.Length(), index = 0; index < count; index++) { RefPtr<FullObjectStoreMetadata>& metadata = metadataArray[index]; if (committed) { metadata->mCommittedAutoIncrementId = metadata->mNextAutoIncrementId; } else { metadata->mNextAutoIncrementId = metadata->mCommittedAutoIncrementId; } } } } #ifdef DEBUG void TransactionBase:: CommitOp::AssertForeignKeyConsistency(DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); MOZ_ASSERT(mTransaction); mTransaction->AssertIsOnConnectionThread(); MOZ_ASSERT(mTransaction->GetMode() != IDBTransaction::READ_ONLY); DatabaseConnection::CachedStatement pragmaStmt; MOZ_ALWAYS_SUCCEEDS( aConnection->GetCachedStatement(NS_LITERAL_CSTRING("PRAGMA foreign_keys;"), &pragmaStmt)); bool hasResult; MOZ_ALWAYS_SUCCEEDS(pragmaStmt->ExecuteStep(&hasResult)); MOZ_ASSERT(hasResult); int32_t foreignKeysEnabled; MOZ_ALWAYS_SUCCEEDS(pragmaStmt->GetInt32(0, &foreignKeysEnabled)); MOZ_ASSERT(foreignKeysEnabled, "Database doesn't have foreign keys enabled!"); DatabaseConnection::CachedStatement checkStmt; MOZ_ALWAYS_SUCCEEDS( aConnection->GetCachedStatement( NS_LITERAL_CSTRING("PRAGMA foreign_key_check;"), &checkStmt)); MOZ_ALWAYS_SUCCEEDS(checkStmt->ExecuteStep(&hasResult)); MOZ_ASSERT(!hasResult, "Database has inconsisistent foreign keys!"); } #endif // DEBUG NS_IMPL_ISUPPORTS_INHERITED0(TransactionBase::CommitOp, DatabaseOperationBase) NS_IMETHODIMP TransactionBase:: CommitOp::Run() { MOZ_ASSERT(mTransaction); mTransaction->AssertIsOnConnectionThread(); PROFILER_LABEL("IndexedDB", "TransactionBase::CommitOp::Run", js::ProfileEntry::Category::STORAGE); IDB_LOG_MARK("IndexedDB %s: Parent Transaction[%lld] Request[%llu]: " "Beginning database work", "IndexedDB %s: P T[%lld] R[%llu]: DB Start", IDB_LOG_ID_STRING(mBackgroundChildLoggingId), mTransaction->LoggingSerialNumber(), mLoggingSerialNumber); if (mTransaction->GetMode() != IDBTransaction::READ_ONLY && mTransaction->mHasBeenActiveOnConnectionThread) { Database* database = mTransaction->GetDatabase(); MOZ_ASSERT(database); if (DatabaseConnection* connection = database->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::READ_WRITE_FLUSH) { 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::CLEANUP) { connection->DoIdleProcessing(/* aNeedsCheckpoint */ true); connection->EnableQuotaChecks(); } } } IDB_LOG_MARK("IndexedDB %s: Parent Transaction[%lld] Request[%llu]: " "Finished database work", "IndexedDB %s: P T[%lld] R[%llu]: DB End", IDB_LOG_ID_STRING(mBackgroundChildLoggingId), mTransaction->LoggingSerialNumber(), mLoggingSerialNumber); IDB_LOG_MARK("IndexedDB %s: Parent Transaction[%lld]: " "Finished database work", "IndexedDB %s: P T[%lld]: DB End", IDB_LOG_ID_STRING(mBackgroundChildLoggingId), mLoggingSerialNumber); return NS_OK; } void TransactionBase:: CommitOp::TransactionFinishedBeforeUnblock() { AssertIsOnBackgroundThread(); MOZ_ASSERT(mTransaction); PROFILER_LABEL("IndexedDB", "CommitOp::TransactionFinishedBeforeUnblock", js::ProfileEntry::Category::STORAGE); if (!IsActorDestroyed()) { mTransaction->UpdateMetadata(mResultCode); } } void TransactionBase:: CommitOp::TransactionFinishedAfterUnblock() { AssertIsOnBackgroundThread(); MOZ_ASSERT(mTransaction); IDB_LOG_MARK("IndexedDB %s: Parent Transaction[%lld]: " "Finished with result 0x%x", "IndexedDB %s: P T[%lld]: Transaction finished (0x%x)", IDB_LOG_ID_STRING(mTransaction->GetLoggingInfo()->Id()), mTransaction->LoggingSerialNumber(), mResultCode); mTransaction->SendCompleteNotification(ClampResultCode(mResultCode)); Database* database = mTransaction->GetDatabase(); MOZ_ASSERT(database); database->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 } DatabaseOp::DatabaseOp(Database* aDatabase) : DatabaseOperationBase(aDatabase->GetLoggingInfo()->Id(), aDatabase->GetLoggingInfo()->NextRequestSN()) , mDatabase(aDatabase) , mState(State::Initial) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aDatabase); } nsresult DatabaseOp::SendToIOThread() { AssertIsOnOwningThread(); MOZ_ASSERT(mState == State::Initial); if (!OperationMayProceed()) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } QuotaManager* quotaManager = QuotaManager::Get(); if (NS_WARN_IF(!quotaManager)) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } // Must set this before dispatching otherwise we will race with the IO thread. mState = State::DatabaseWork; nsresult rv = quotaManager->IOThread()->Dispatch(this, NS_DISPATCH_NORMAL); if (NS_WARN_IF(NS_FAILED(rv))) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } return NS_OK; } NS_IMETHODIMP DatabaseOp::Run() { nsresult rv; switch (mState) { case State::Initial: rv = SendToIOThread(); break; case State::DatabaseWork: rv = DoDatabaseWork(); break; case State::SendingResults: SendResults(); return NS_OK; default: MOZ_CRASH("Bad state!"); } if (NS_WARN_IF(NS_FAILED(rv)) && mState != State::SendingResults) { if (NS_SUCCEEDED(mResultCode)) { mResultCode = rv; } // Must set mState before dispatching otherwise we will race with the owning // thread. mState = State::SendingResults; MOZ_ALWAYS_SUCCEEDS(mOwningThread->Dispatch(this, NS_DISPATCH_NORMAL)); } return NS_OK; } void DatabaseOp::ActorDestroy(ActorDestroyReason aWhy) { AssertIsOnBackgroundThread(); NoteActorDestroyed(); } CreateFileOp::CreateFileOp(Database* aDatabase, const DatabaseRequestParams& aParams) : DatabaseOp(aDatabase) , mParams(aParams.get_CreateFileParams()) { MOZ_ASSERT(aParams.type() == DatabaseRequestParams::TCreateFileParams); } nsresult CreateFileOp::CreateMutableFile(MutableFile** aMutableFile) { nsCOMPtr<nsIFile> file = GetFileForFileInfo(mFileInfo); if (NS_WARN_IF(!file)) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } RefPtr<MutableFile> mutableFile = MutableFile::Create(file, mDatabase, mFileInfo); if (NS_WARN_IF(!mutableFile)) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } // Transfer ownership to IPDL. mutableFile->SetActorAlive(); if (!mDatabase->SendPBackgroundMutableFileConstructor(mutableFile, mParams.name(), mParams.type())) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } mutableFile.forget(aMutableFile); return NS_OK; } nsresult CreateFileOp::DoDatabaseWork() { AssertIsOnIOThread(); MOZ_ASSERT(mState == State::DatabaseWork); PROFILER_LABEL("IndexedDB", "CreateFileOp::DoDatabaseWork", js::ProfileEntry::Category::STORAGE); if (NS_WARN_IF(IndexedDatabaseManager::InLowDiskSpaceMode())) { NS_WARNING("Refusing to create file because disk space is low!"); return NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR; } if (NS_WARN_IF(QuotaManager::IsShuttingDown()) || !OperationMayProceed()) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } FileManager* fileManager = mDatabase->GetFileManager(); mFileInfo = fileManager->GetNewFileInfo(); if (NS_WARN_IF(!mFileInfo)) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } const int64_t fileId = mFileInfo->Id(); nsCOMPtr<nsIFile> journalDirectory = fileManager->EnsureJournalDirectory(); if (NS_WARN_IF(!journalDirectory)) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } nsCOMPtr<nsIFile> journalFile = fileManager->GetFileForId(journalDirectory, fileId); if (NS_WARN_IF(!journalFile)) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } nsresult rv = journalFile->Create(nsIFile::NORMAL_FILE_TYPE, 0644); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsCOMPtr<nsIFile> fileDirectory = fileManager->GetDirectory(); if (NS_WARN_IF(!fileDirectory)) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } nsCOMPtr<nsIFile> file = fileManager->GetFileForId(fileDirectory, fileId); if (NS_WARN_IF(!file)) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } rv = file->Create(nsIFile::NORMAL_FILE_TYPE, 0644); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Must set mState before dispatching otherwise we will race with the owning // thread. mState = State::SendingResults; rv = mOwningThread->Dispatch(this, NS_DISPATCH_NORMAL); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } void CreateFileOp::SendResults() { AssertIsOnOwningThread(); MOZ_ASSERT(mState == State::SendingResults); if (!IsActorDestroyed() && !mDatabase->IsInvalidated()) { DatabaseRequestResponse response; if (NS_SUCCEEDED(mResultCode)) { RefPtr<MutableFile> mutableFile; nsresult rv = CreateMutableFile(getter_AddRefs(mutableFile)); if (NS_SUCCEEDED(rv)) { // We successfully created a mutable file so use its actor as the // success result for this request. CreateFileRequestResponse createResponse; createResponse.mutableFileParent() = mutableFile; response = createResponse; } else { response = ClampResultCode(rv); #ifdef DEBUG mResultCode = response.get_nsresult(); #endif } } else { response = ClampResultCode(mResultCode); } Unused << PBackgroundIDBDatabaseRequestParent::Send__delete__(this, response); } mState = State::Completed; } 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(); PROFILER_LABEL("IndexedDB", "CreateObjectStoreOp::DoDatabaseWork", js::ProfileEntry::Category::STORAGE); if (NS_WARN_IF(IndexedDatabaseManager::InLowDiskSpaceMode())) { return NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR; } #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... DatabaseConnection::CachedStatement stmt; MOZ_ALWAYS_SUCCEEDS( aConnection->GetCachedStatement(NS_LITERAL_CSTRING( "SELECT name " "FROM object_store " "WHERE name = :name;"), &stmt)); MOZ_ALWAYS_SUCCEEDS( stmt->BindStringByName(NS_LITERAL_CSTRING("name"), mMetadata.name())); bool hasResult; MOZ_ALWAYS_SUCCEEDS(stmt->ExecuteStep(&hasResult)); MOZ_ASSERT(!hasResult); } #endif DatabaseConnection::AutoSavepoint autoSave; nsresult rv = autoSave.Start(Transaction()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } DatabaseConnection::CachedStatement stmt; rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( "INSERT INTO object_store (id, auto_increment, name, key_path) " "VALUES (:id, :auto_increment, :name, :key_path);"), &stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("id"), mMetadata.id()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("auto_increment"), mMetadata.autoIncrement() ? 1 : 0); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->BindStringByName(NS_LITERAL_CSTRING("name"), mMetadata.name()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } NS_NAMED_LITERAL_CSTRING(keyPath, "key_path"); if (mMetadata.keyPath().IsValid()) { nsAutoString keyPathSerialization; mMetadata.keyPath().SerializeToString(keyPathSerialization); rv = stmt->BindStringByName(keyPath, keyPathSerialization); } else { rv = stmt->BindNullByName(keyPath); } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->Execute(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } #ifdef DEBUG { int64_t id; MOZ_ALWAYS_SUCCEEDS( aConnection->GetStorageConnection()->GetLastInsertRowID(&id)); MOZ_ASSERT(mMetadata.id() == id); } #endif rv = autoSave.Commit(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } nsresult DeleteObjectStoreOp::DoDatabaseWork(DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); PROFILER_LABEL("IndexedDB", "DeleteObjectStoreOp::DoDatabaseWork", js::ProfileEntry::Category::STORAGE); NS_NAMED_LITERAL_CSTRING(objectStoreIdString, "object_store_id"); #ifdef DEBUG { // Make sure |mIsLastObjectStore| is telling the truth. DatabaseConnection::CachedStatement stmt; MOZ_ALWAYS_SUCCEEDS( aConnection->GetCachedStatement(NS_LITERAL_CSTRING( "SELECT id " "FROM object_store;"), &stmt)); 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; nsresult rv = autoSave.Start(Transaction()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (mIsLastObjectStore) { // We can just delete everything if this is the last object store. DatabaseConnection::CachedStatement stmt; rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( "DELETE FROM index_data;"), &stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->Execute(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( "DELETE FROM unique_index_data;"), &stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->Execute(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( "DELETE FROM object_data;"), &stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->Execute(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( "DELETE FROM object_store_index;"), &stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->Execute(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( "DELETE FROM object_store;"), &stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->Execute(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } else { bool hasIndexes; rv = ObjectStoreHasIndexes(aConnection, mMetadata->mCommonMetadata.id(), &hasIndexes); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (hasIndexes) { rv = DeleteObjectStoreDataTableRowsWithIndexes( aConnection, mMetadata->mCommonMetadata.id(), void_t()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Now clean up the object store index table. DatabaseConnection::CachedStatement stmt; rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( "DELETE FROM object_store_index " "WHERE object_store_id = :object_store_id;"), &stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->BindInt64ByName(objectStoreIdString, mMetadata->mCommonMetadata.id()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->Execute(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } else { // We only have to worry about object data if this object store has no // indexes. DatabaseConnection::CachedStatement stmt; rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( "DELETE FROM object_data " "WHERE object_store_id = :object_store_id;"), &stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->BindInt64ByName(objectStoreIdString, mMetadata->mCommonMetadata.id()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->Execute(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } DatabaseConnection::CachedStatement stmt; rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( "DELETE FROM object_store " "WHERE id = :object_store_id;"), &stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->BindInt64ByName(objectStoreIdString, mMetadata->mCommonMetadata.id()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->Execute(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } #ifdef DEBUG { int32_t deletedRowCount; MOZ_ALWAYS_SUCCEEDS( aConnection->GetStorageConnection()-> GetAffectedRows(&deletedRowCount)); MOZ_ASSERT(deletedRowCount == 1); } #endif } rv = autoSave.Commit(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (mMetadata->mCommonMetadata.autoIncrement()) { Transaction()->ForgetModifiedAutoIncrementObjectStore(mMetadata); } return NS_OK; } nsresult RenameObjectStoreOp::DoDatabaseWork(DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); PROFILER_LABEL("IndexedDB", "RenameObjectStoreOp::DoDatabaseWork", js::ProfileEntry::Category::STORAGE); if (NS_WARN_IF(IndexedDatabaseManager::InLowDiskSpaceMode())) { return NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR; } #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... DatabaseConnection::CachedStatement stmt; MOZ_ALWAYS_SUCCEEDS( aConnection->GetCachedStatement(NS_LITERAL_CSTRING( "SELECT name " "FROM object_store " "WHERE name = :name " "AND id != :id;"), &stmt)); MOZ_ALWAYS_SUCCEEDS( stmt->BindStringByName(NS_LITERAL_CSTRING("name"), mNewName)); MOZ_ALWAYS_SUCCEEDS( stmt->BindInt64ByName(NS_LITERAL_CSTRING("id"), mId)); bool hasResult; MOZ_ALWAYS_SUCCEEDS(stmt->ExecuteStep(&hasResult)); MOZ_ASSERT(!hasResult); } #endif DatabaseConnection::AutoSavepoint autoSave; nsresult rv = autoSave.Start(Transaction()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } DatabaseConnection::CachedStatement stmt; rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( "UPDATE object_store " "SET name = :name " "WHERE id = :id;"), &stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->BindStringByName(NS_LITERAL_CSTRING("name"), mNewName); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("id"), mId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->Execute(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = autoSave.Commit(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } CreateIndexOp::CreateIndexOp(VersionChangeTransaction* aTransaction, const int64_t aObjectStoreId, const IndexMetadata& aMetadata) : VersionChangeTransactionOp(aTransaction) , mMetadata(aMetadata) , mFileManager(aTransaction->GetDatabase()->GetFileManager()) , mDatabaseId(aTransaction->DatabaseId()) , mObjectStoreId(aObjectStoreId) { MOZ_ASSERT(aObjectStoreId); MOZ_ASSERT(aMetadata.id()); MOZ_ASSERT(mFileManager); MOZ_ASSERT(!mDatabaseId.IsEmpty()); } unsigned int CreateIndexOp::sThreadLocalIndex = kBadThreadLocalIndex; nsresult CreateIndexOp::InsertDataFromObjectStore(DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(!IndexedDatabaseManager::InLowDiskSpaceMode()); MOZ_ASSERT(mMaybeUniqueIndexTable); PROFILER_LABEL("IndexedDB", "CreateIndexOp::InsertDataFromObjectStore", js::ProfileEntry::Category::STORAGE); nsCOMPtr<mozIStorageConnection> storageConnection = aConnection->GetStorageConnection(); MOZ_ASSERT(storageConnection); ThreadLocalJSContext* context = ThreadLocalJSContext::GetOrCreate(); if (NS_WARN_IF(!context)) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } JSContext* cx = context->Context(); JSAutoRequest ar(cx); JSAutoCompartment ac(cx, context->Global()); RefPtr<UpdateIndexDataValuesFunction> updateFunction = new UpdateIndexDataValuesFunction(this, aConnection, cx); NS_NAMED_LITERAL_CSTRING(updateFunctionName, "update_index_data_values"); 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) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(!IndexedDatabaseManager::InLowDiskSpaceMode()); MOZ_ASSERT(mMaybeUniqueIndexTable); DebugOnly<void*> storageConnection = aConnection->GetStorageConnection(); MOZ_ASSERT(storageConnection); DatabaseConnection::CachedStatement stmt; nsresult rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( "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;"), &stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("object_store_id"), mObjectStoreId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->Execute(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } bool CreateIndexOp::Init(TransactionBase* aTransaction) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aTransaction); struct MOZ_STACK_CLASS Helper final { static void Destroy(void* aThreadLocal) { delete static_cast<ThreadLocalJSContext*>(aThreadLocal); } }; if (sThreadLocalIndex == kBadThreadLocalIndex) { if (NS_WARN_IF(PR_SUCCESS != PR_NewThreadPrivateIndex(&sThreadLocalIndex, &Helper::Destroy))) { return false; } } MOZ_ASSERT(sThreadLocalIndex != kBadThreadLocalIndex); nsresult rv = GetUniqueIndexTableForObjectStore(aTransaction, mObjectStoreId, mMaybeUniqueIndexTable); if (NS_WARN_IF(NS_FAILED(rv))) { return false; } return true; } nsresult CreateIndexOp::DoDatabaseWork(DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); PROFILER_LABEL("IndexedDB", "CreateIndexOp::DoDatabaseWork", js::ProfileEntry::Category::STORAGE); if (NS_WARN_IF(IndexedDatabaseManager::InLowDiskSpaceMode())) { return NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR; } #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... DatabaseConnection::CachedStatement stmt; MOZ_ALWAYS_SUCCEEDS( aConnection->GetCachedStatement(NS_LITERAL_CSTRING( "SELECT name " "FROM object_store_index " "WHERE object_store_id = :osid " "AND name = :name;"), &stmt)); MOZ_ALWAYS_SUCCEEDS( stmt->BindInt64ByName(NS_LITERAL_CSTRING("osid"), mObjectStoreId)); MOZ_ALWAYS_SUCCEEDS( stmt->BindStringByName(NS_LITERAL_CSTRING("name"), mMetadata.name())); bool hasResult; MOZ_ALWAYS_SUCCEEDS(stmt->ExecuteStep(&hasResult)); MOZ_ASSERT(!hasResult); } #endif DatabaseConnection::AutoSavepoint autoSave; nsresult rv = autoSave.Start(Transaction()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } DatabaseConnection::CachedStatement stmt; rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( "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, :osid, :locale, " ":is_auto_locale)"), &stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("id"), mMetadata.id()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->BindStringByName(NS_LITERAL_CSTRING("name"), mMetadata.name()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsAutoString keyPathSerialization; mMetadata.keyPath().SerializeToString(keyPathSerialization); rv = stmt->BindStringByName(NS_LITERAL_CSTRING("key_path"), keyPathSerialization); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("unique"), mMetadata.unique() ? 1 : 0); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("multientry"), mMetadata.multiEntry() ? 1 : 0); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("osid"), mObjectStoreId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (mMetadata.locale().IsEmpty()) { rv = stmt->BindNullByName(NS_LITERAL_CSTRING("locale")); } else { rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("locale"), mMetadata.locale()); } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("is_auto_locale"), mMetadata.autoLocale()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->Execute(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } #ifdef DEBUG { int64_t id; MOZ_ALWAYS_SUCCEEDS( aConnection->GetStorageConnection()->GetLastInsertRowID(&id)); MOZ_ASSERT(mMetadata.id() == id); } #endif rv = InsertDataFromObjectStore(aConnection); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = autoSave.Commit(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } static const JSClassOps sNormalJSContextGlobalClassOps = { /* addProperty */ nullptr, /* delProperty */ nullptr, /* getProperty */ nullptr, /* setProperty */ nullptr, /* enumerate */ nullptr, /* resolve */ nullptr, /* mayResolve */ nullptr, /* finalize */ nullptr, /* call */ nullptr, /* hasInstance */ nullptr, /* construct */ nullptr, /* trace */ JS_GlobalObjectTraceHook }; const JSClass NormalJSContext::sGlobalClass = { "IndexedDBTransactionThreadGlobal", JSCLASS_GLOBAL_FLAGS, &sNormalJSContextGlobalClassOps }; bool NormalJSContext::Init() { MOZ_ASSERT(!IsOnBackgroundThread()); mContext = JS_NewContext(kContextHeapSize); if (NS_WARN_IF(!mContext)) { return false; } // Let everyone know that we might be able to call JS. This alerts the // profiler about certain possible deadlocks. NS_GetCurrentThread()->SetCanInvokeJS(true); // Not setting this will cause JS_CHECK_RECURSION to report false positives. JS_SetNativeStackQuota(mContext, 128 * sizeof(size_t) * 1024); if (NS_WARN_IF(!JS::InitSelfHostedCode(mContext))) { return false; } JSAutoRequest ar(mContext); JS::CompartmentOptions options; mGlobal = JS_NewGlobalObject(mContext, &sGlobalClass, nullptr, JS::FireOnNewGlobalHook, options); if (NS_WARN_IF(!mGlobal)) { return false; } return true; } // static NormalJSContext* NormalJSContext::Create() { MOZ_ASSERT(!IsOnBackgroundThread()); nsAutoPtr<NormalJSContext> newContext(new NormalJSContext()); if (NS_WARN_IF(!newContext->Init())) { return nullptr; } return newContext.forget(); } // static auto CreateIndexOp:: ThreadLocalJSContext::GetOrCreate() -> ThreadLocalJSContext* { MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(CreateIndexOp::kBadThreadLocalIndex != CreateIndexOp::sThreadLocalIndex); auto* context = static_cast<ThreadLocalJSContext*>( PR_GetThreadPrivate(CreateIndexOp::sThreadLocalIndex)); if (context) { return context; } nsAutoPtr<ThreadLocalJSContext> newContext(new ThreadLocalJSContext()); if (NS_WARN_IF(!newContext->Init())) { return nullptr; } DebugOnly<PRStatus> status = PR_SetThreadPrivate(CreateIndexOp::sThreadLocalIndex, newContext); MOZ_ASSERT(status == PR_SUCCESS); return newContext.forget(); } 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(mCx); PROFILER_LABEL("IndexedDB", "CreateIndexOp::UpdateIndexDataValuesFunction::OnFunctionCall", js::ProfileEntry::Category::STORAGE); #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 StructuredCloneReadInfo cloneInfo; nsresult rv = GetStructuredCloneReadInfoFromValueArray(aValues, /* aDataIndex */ 3, /* aFileIdsIndex */ 2, mOp->mFileManager, &cloneInfo); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } JS::Rooted<JS::Value> clone(mCx); if (NS_WARN_IF(!IDBObjectStore::DeserializeIndexValue(mCx, cloneInfo, &clone))) { return NS_ERROR_DOM_DATA_CLONE_ERR; } const IndexMetadata& metadata = mOp->mMetadata; const int64_t& objectStoreId = mOp->mObjectStoreId; AutoTArray<IndexUpdateInfo, 32> updateInfos; rv = IDBObjectStore::AppendIndexUpdateInfo(metadata.id(), metadata.keyPath(), metadata.unique(), metadata.multiEntry(), metadata.locale(), mCx, clone, updateInfos); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (updateInfos.IsEmpty()) { // XXX See if we can do this without copying... nsCOMPtr<nsIVariant> unmodifiedValue; // No changes needed, just return the original value. int32_t valueType; rv = aValues->GetTypeOfIndex(1, &valueType); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } 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; rv = aValues->GetSharedBlob(1, &blobDataLength, &blobData); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } std::pair<uint8_t *, int> copiedBlobDataPair( static_cast<uint8_t*>(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; rv = key.SetFromValueArray(aValues, 0); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } AutoTArray<IndexDataValue, 32> indexValues; rv = ReadCompressedIndexDataValues(aValues, 1, indexValues); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } const bool hadPreviousIndexValues = !indexValues.IsEmpty(); const uint32_t updateInfoCount = updateInfos.Length(); if (NS_WARN_IF(!indexValues.SetCapacity(indexValues.Length() + updateInfoCount, fallible))) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_OUT_OF_MEMORY; } // First construct the full list to update the index_data_values row. for (uint32_t index = 0; index < updateInfoCount; index++) { const IndexUpdateInfo& info = updateInfos[index]; MOZ_ALWAYS_TRUE( indexValues.InsertElementSorted(IndexDataValue(metadata.id(), metadata.unique(), info.value(), info.localizedValue()), fallible)); } UniqueFreePtr<uint8_t> indexValuesBlob; uint32_t indexValuesBlobLength; rv = MakeCompressedIndexDataValues(indexValues, indexValuesBlob, &indexValuesBlobLength); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(!indexValuesBlobLength == !(indexValuesBlob.get())); nsCOMPtr<nsIVariant> 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 (uint32_t index = 0; index < updateInfoCount; index++) { const IndexUpdateInfo& info = updateInfos[index]; MOZ_ALWAYS_TRUE( indexValues.InsertElementSorted(IndexDataValue(metadata.id(), metadata.unique(), info.value(), info.localizedValue()), fallible)); } } rv = InsertIndexTableRows(mConnection, objectStoreId, key, indexValues); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } std::pair<uint8_t *, int> copiedBlobDataPair(indexValuesBlob.release(), indexValuesBlobLength); value = new storage::AdoptedBlobVariant(copiedBlobDataPair); value.forget(_retval); return NS_OK; } DeleteIndexOp::DeleteIndexOp(VersionChangeTransaction* aTransaction, const int64_t aObjectStoreId, const int64_t aIndexId, const bool aUnique, const bool aIsLastIndex) : VersionChangeTransactionOp(aTransaction) , mObjectStoreId(aObjectStoreId) , mIndexId(aIndexId) , mUnique(aUnique) , mIsLastIndex(aIsLastIndex) { MOZ_ASSERT(aObjectStoreId); MOZ_ASSERT(aIndexId); } nsresult DeleteIndexOp::RemoveReferencesToIndex(DatabaseConnection* aConnection, const Key& aObjectStoreKey, nsTArray<IndexDataValue>& aIndexValues) { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(aConnection); MOZ_ASSERT(!aObjectStoreKey.IsUnset()); MOZ_ASSERT_IF(!mIsLastIndex, !aIndexValues.IsEmpty()); struct MOZ_STACK_CLASS IndexIdComparator final { bool Equals(const IndexDataValue& aA, const IndexDataValue& aB) const { // Ignore everything but the index id. return aA.mIndexId == aB.mIndexId; }; bool LessThan(const IndexDataValue& aA, const IndexDataValue& aB) const { return aA.mIndexId < aB.mIndexId; }; }; PROFILER_LABEL("IndexedDB", "DeleteIndexOp::RemoveReferencesToIndex", js::ProfileEntry::Category::STORAGE); 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. DatabaseConnection::CachedStatement stmt; nsresult rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( "UPDATE object_data " "SET index_data_values = NULL " "WHERE object_store_id = :object_store_id " "AND key = :key;"), &stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("object_store_id"), mObjectStoreId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aObjectStoreKey.BindToStatement(stmt, NS_LITERAL_CSTRING("key")); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->Execute(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } IndexDataValue search; search.mIndexId = mIndexId; // This returns the first element that matches our index id found during a // binary search. However, there could still be other elements before that. size_t firstElementIndex = aIndexValues.BinaryIndexOf(search, IndexIdComparator()); if (NS_WARN_IF(firstElementIndex == aIndexValues.NoIndex) || NS_WARN_IF(aIndexValues[firstElementIndex].mIndexId != mIndexId)) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_FILE_CORRUPTED; } MOZ_ASSERT(aIndexValues[firstElementIndex].mIndexId == mIndexId); // Walk backwards to find the real first index. while (firstElementIndex) { if (aIndexValues[firstElementIndex - 1].mIndexId == mIndexId) { firstElementIndex--; } else { break; } } MOZ_ASSERT(aIndexValues[firstElementIndex].mIndexId == mIndexId); const size_t indexValuesLength = aIndexValues.Length(); // Find the last element with the same index id. size_t lastElementIndex = firstElementIndex; while (lastElementIndex < indexValuesLength) { if (aIndexValues[lastElementIndex].mIndexId == mIndexId) { lastElementIndex++; } else { break; } } MOZ_ASSERT(lastElementIndex > firstElementIndex); MOZ_ASSERT_IF(lastElementIndex < indexValuesLength, aIndexValues[lastElementIndex].mIndexId != mIndexId); MOZ_ASSERT(aIndexValues[lastElementIndex - 1].mIndexId == mIndexId); aIndexValues.RemoveElementsAt(firstElementIndex, lastElementIndex - firstElementIndex); nsresult rv = UpdateIndexValues(aConnection, mObjectStoreId, aObjectStoreKey, aIndexValues); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } nsresult DeleteIndexOp::DoDatabaseWork(DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); #ifdef DEBUG { // Make sure |mIsLastIndex| is telling the truth. DatabaseConnection::CachedStatement stmt; MOZ_ALWAYS_SUCCEEDS( aConnection->GetCachedStatement(NS_LITERAL_CSTRING( "SELECT id " "FROM object_store_index " "WHERE object_store_id = :object_store_id;"), &stmt)); MOZ_ALWAYS_SUCCEEDS( stmt->BindInt64ByName(NS_LITERAL_CSTRING("object_store_id"), 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 PROFILER_LABEL("IndexedDB", "DeleteIndexOp::DoDatabaseWork", js::ProfileEntry::Category::STORAGE); DatabaseConnection::AutoSavepoint autoSave; nsresult rv = autoSave.Start(Transaction()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } DatabaseConnection::CachedStatement selectStmt; // 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. if (mUnique) { if (mIsLastIndex) { rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( "/* do not warn (bug someone else) */ " "SELECT value, object_data_key " "FROM unique_index_data " "WHERE index_id = :index_id " "ORDER BY object_data_key ASC;"), &selectStmt); } else { rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( "/* 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 = :index_id " "AND object_data.object_store_id = :object_store_id " "ORDER BY unique_index_data.object_data_key ASC;"), &selectStmt); } } else { if (mIsLastIndex) { rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( "/* do not warn (bug me not) */ " "SELECT value, object_data_key " "FROM index_data " "WHERE index_id = :index_id " "AND object_store_id = :object_store_id " "ORDER BY object_data_key ASC;"), &selectStmt); } else { rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( "/* 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 = :index_id " "AND object_data.object_store_id = :object_store_id " "ORDER BY index_data.object_data_key ASC;"), &selectStmt); } } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } NS_NAMED_LITERAL_CSTRING(indexIdString, "index_id"); rv = selectStmt->BindInt64ByName(indexIdString, mIndexId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (!mUnique || !mIsLastIndex) { rv = selectStmt->BindInt64ByName(NS_LITERAL_CSTRING("object_store_id"), mObjectStoreId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } NS_NAMED_LITERAL_CSTRING(valueString, "value"); NS_NAMED_LITERAL_CSTRING(objectDataKeyString, "object_data_key"); DatabaseConnection::CachedStatement deleteIndexRowStmt; DatabaseConnection::CachedStatement nullIndexDataValuesStmt; Key lastObjectStoreKey; AutoTArray<IndexDataValue, 32> lastIndexValues; bool hasResult; while (NS_SUCCEEDED(rv = selectStmt->ExecuteStep(&hasResult)) && hasResult) { // We always need the index key to delete the index row. Key indexKey; rv = indexKey.SetFromStatement(selectStmt, 0); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (NS_WARN_IF(indexKey.IsUnset())) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_FILE_CORRUPTED; } // 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; rv = selectStmt->GetSharedBlob(1, &objectStoreKeyDataLength, &objectStoreKeyData); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (NS_WARN_IF(!objectStoreKeyDataLength)) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_FILE_CORRUPTED; } nsDependentCString currentObjectStoreKeyBuffer( reinterpret_cast<const char*>(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. rv = RemoveReferencesToIndex(aConnection, lastObjectStoreKey, lastIndexValues); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } // 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(); rv = ReadCompressedIndexDataValues(selectStmt, 2, lastIndexValues); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (NS_WARN_IF(lastIndexValues.IsEmpty())) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_FILE_CORRUPTED; } } } // Now delete the index row. if (deleteIndexRowStmt) { MOZ_ALWAYS_SUCCEEDS(deleteIndexRowStmt->Reset()); } else { if (mUnique) { rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( "DELETE FROM unique_index_data " "WHERE index_id = :index_id " "AND value = :value;"), &deleteIndexRowStmt); } else { rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( "DELETE FROM index_data " "WHERE index_id = :index_id " "AND value = :value " "AND object_data_key = :object_data_key;"), &deleteIndexRowStmt); } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } rv = deleteIndexRowStmt->BindInt64ByName(indexIdString, mIndexId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = indexKey.BindToStatement(deleteIndexRowStmt, valueString); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (!mUnique) { rv = lastObjectStoreKey.BindToStatement(deleteIndexRowStmt, objectDataKeyString); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } rv = deleteIndexRowStmt->Execute(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Take care of the last key. if (!lastObjectStoreKey.IsUnset()) { MOZ_ASSERT_IF(!mIsLastIndex, !lastIndexValues.IsEmpty()); rv = RemoveReferencesToIndex(aConnection, lastObjectStoreKey, lastIndexValues); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } DatabaseConnection::CachedStatement deleteStmt; rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( "DELETE FROM object_store_index " "WHERE id = :index_id;"), &deleteStmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = deleteStmt->BindInt64ByName(indexIdString, mIndexId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = deleteStmt->Execute(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } #ifdef DEBUG { int32_t deletedRowCount; MOZ_ALWAYS_SUCCEEDS( aConnection->GetStorageConnection()->GetAffectedRows(&deletedRowCount)); MOZ_ASSERT(deletedRowCount == 1); } #endif rv = autoSave.Commit(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } nsresult RenameIndexOp::DoDatabaseWork(DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); PROFILER_LABEL("IndexedDB", "RenameIndexOp::DoDatabaseWork", js::ProfileEntry::Category::STORAGE); if (NS_WARN_IF(IndexedDatabaseManager::InLowDiskSpaceMode())) { return NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR; } #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... DatabaseConnection::CachedStatement stmt; MOZ_ALWAYS_SUCCEEDS( aConnection->GetCachedStatement(NS_LITERAL_CSTRING( "SELECT name " "FROM object_store_index " "WHERE object_store_id = :object_store_id " "AND name = :name " "AND id != :id;"), &stmt)); MOZ_ALWAYS_SUCCEEDS( stmt->BindInt64ByName(NS_LITERAL_CSTRING("object_store_id"), mObjectStoreId)); MOZ_ALWAYS_SUCCEEDS( stmt->BindStringByName(NS_LITERAL_CSTRING("name"), mNewName)); MOZ_ALWAYS_SUCCEEDS( stmt->BindInt64ByName(NS_LITERAL_CSTRING("id"), mIndexId)); bool hasResult; MOZ_ALWAYS_SUCCEEDS(stmt->ExecuteStep(&hasResult)); MOZ_ASSERT(!hasResult); } #else Unused << mObjectStoreId; #endif DatabaseConnection::AutoSavepoint autoSave; nsresult rv = autoSave.Start(Transaction()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } DatabaseConnection::CachedStatement stmt; rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( "UPDATE object_store_index " "SET name = :name " "WHERE id = :id;"), &stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->BindStringByName(NS_LITERAL_CSTRING("name"), mNewName); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("id"), mIndexId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->Execute(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = autoSave.Commit(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } // static nsresult NormalTransactionOp::ObjectStoreHasIndexes(NormalTransactionOp* aOp, DatabaseConnection* aConnection, const int64_t aObjectStoreId, const bool aMayHaveIndexes, bool* aHasIndexes) { MOZ_ASSERT(aOp); MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(aObjectStoreId); MOZ_ASSERT(aHasIndexes); bool hasIndexes; if (aOp->Transaction()->GetMode() == IDBTransaction::VERSION_CHANGE && 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. nsresult rv = DatabaseOperationBase::ObjectStoreHasIndexes(aConnection, aObjectStoreId, &hasIndexes); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } else { MOZ_ASSERT(NS_SUCCEEDED( DatabaseOperationBase::ObjectStoreHasIndexes(aConnection, aObjectStoreId, &hasIndexes))); MOZ_ASSERT(aMayHaveIndexes == hasIndexes); hasIndexes = aMayHaveIndexes; } *aHasIndexes = hasIndexes; return NS_OK; } nsresult NormalTransactionOp::GetPreprocessParams(PreprocessParams& aParams) { return NS_OK; } nsresult NormalTransactionOp::SendPreprocessInfo() { AssertIsOnOwningThread(); MOZ_ASSERT(!IsActorDestroyed()); PreprocessParams params; nsresult rv = GetPreprocessParams(params); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } 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()) { RequestResponse response; GetResponse(response); 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(); } bool NormalTransactionOp::RecvContinue(const PreprocessResponse& aResponse) { AssertIsOnOwningThread(); switch (aResponse.type()) { case PreprocessResponse::Tnsresult: mResultCode = aResponse.get_nsresult(); break; case PreprocessResponse::TObjectStoreGetPreprocessResponse: break; case PreprocessResponse::TObjectStoreGetAllPreprocessResponse: break; default: MOZ_CRASH("Should never get here!"); } NoteContinueReceived(); return true; } ObjectStoreAddOrPutRequestOp::ObjectStoreAddOrPutRequestOp( TransactionBase* aTransaction, const RequestParams& aParams) : NormalTransactionOp(aTransaction) , mParams(aParams.type() == RequestParams::TObjectStoreAddParams ? aParams.get_ObjectStoreAddParams().commonParams() : aParams.get_ObjectStorePutParams().commonParams()) , mGroup(aTransaction->GetDatabase()->Group()) , mOrigin(aTransaction->GetDatabase()->Origin()) , mPersistenceType(aTransaction->GetDatabase()->Type()) , mOverwrite(aParams.type() == RequestParams::TObjectStorePutParams) , mObjectStoreMayHaveIndexes(false) { MOZ_ASSERT(aParams.type() == RequestParams::TObjectStoreAddParams || aParams.type() == RequestParams::TObjectStorePutParams); mMetadata = aTransaction->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 { bool hasIndexes = false; MOZ_ASSERT(NS_SUCCEEDED( DatabaseOperationBase::ObjectStoreHasIndexes(aConnection, mParams.objectStoreId(), &hasIndexes))); MOZ_ASSERT(hasIndexes, "Don't use this slow method if there are no indexes!"); } #endif DatabaseConnection::CachedStatement indexValuesStmt; nsresult rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( "SELECT index_data_values " "FROM object_data " "WHERE object_store_id = :object_store_id " "AND key = :key;"), &indexValuesStmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = indexValuesStmt->BindInt64ByName(NS_LITERAL_CSTRING("object_store_id"), mParams.objectStoreId()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = mResponse.BindToStatement(indexValuesStmt, NS_LITERAL_CSTRING("key")); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } bool hasResult; rv = indexValuesStmt->ExecuteStep(&hasResult); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (hasResult) { AutoTArray<IndexDataValue, 32> existingIndexValues; rv = ReadCompressedIndexDataValues(indexValuesStmt, 0, existingIndexValues); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = DeleteIndexDataTableRows(aConnection, mResponse, existingIndexValues); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } return NS_OK; } bool ObjectStoreAddOrPutRequestOp::Init(TransactionBase* aTransaction) { AssertIsOnOwningThread(); const nsTArray<IndexUpdateInfo>& indexUpdateInfos = mParams.indexUpdateInfos(); if (!indexUpdateInfos.IsEmpty()) { const uint32_t count = indexUpdateInfos.Length(); mUniqueIndexTable.emplace(); for (uint32_t index = 0; index < count; index++) { const IndexUpdateInfo& updateInfo = indexUpdateInfos[index]; RefPtr<FullIndexMetadata> indexMetadata; MOZ_ALWAYS_TRUE(mMetadata->mIndexes.Get(updateInfo.indexId(), getter_AddRefs(indexMetadata))); MOZ_ASSERT(!indexMetadata->mDeleted); const int64_t& indexId = indexMetadata->mCommonMetadata.id(); const bool& unique = indexMetadata->mCommonMetadata.unique(); MOZ_ASSERT(indexId == updateInfo.indexId()); MOZ_ASSERT_IF(!indexMetadata->mCommonMetadata.multiEntry(), !mUniqueIndexTable.ref().Get(indexId)); if (NS_WARN_IF(!mUniqueIndexTable.ref().Put(indexId, unique, fallible))) { return false; } } } else if (mOverwrite) { mUniqueIndexTable.emplace(); } #ifdef DEBUG if (mUniqueIndexTable.isSome()) { mUniqueIndexTable.ref().MarkImmutable(); } #endif const nsTArray<FileAddInfo>& fileAddInfos = mParams.fileAddInfos(); if (!fileAddInfos.IsEmpty()) { const uint32_t count = fileAddInfos.Length(); if (NS_WARN_IF(!mStoredFileInfos.SetCapacity(count, fallible))) { return false; } for (uint32_t index = 0; index < count; index++) { const FileAddInfo& fileAddInfo = fileAddInfos[index]; MOZ_ASSERT(fileAddInfo.type() == StructuredCloneFile::eBlob || fileAddInfo.type() == StructuredCloneFile::eMutableFile || fileAddInfo.type() == StructuredCloneFile::eWasmBytecode || fileAddInfo.type() == StructuredCloneFile::eWasmCompiled); const DatabaseOrMutableFile& file = fileAddInfo.file(); StoredFileInfo* storedFileInfo = mStoredFileInfos.AppendElement(fallible); MOZ_ASSERT(storedFileInfo); switch (fileAddInfo.type()) { case StructuredCloneFile::eBlob: { MOZ_ASSERT(file.type() == DatabaseOrMutableFile::TPBackgroundIDBDatabaseFileParent); storedFileInfo->mFileActor = static_cast<DatabaseFile*>( file.get_PBackgroundIDBDatabaseFileParent()); MOZ_ASSERT(storedFileInfo->mFileActor); storedFileInfo->mFileInfo = storedFileInfo->mFileActor->GetFileInfo(); MOZ_ASSERT(storedFileInfo->mFileInfo); storedFileInfo->mType = StructuredCloneFile::eBlob; break; } case StructuredCloneFile::eMutableFile: { MOZ_ASSERT(file.type() == DatabaseOrMutableFile::TPBackgroundMutableFileParent); auto mutableFileActor = static_cast<MutableFile*>( file.get_PBackgroundMutableFileParent()); MOZ_ASSERT(mutableFileActor); storedFileInfo->mFileInfo = mutableFileActor->GetFileInfo(); MOZ_ASSERT(storedFileInfo->mFileInfo); storedFileInfo->mType = StructuredCloneFile::eMutableFile; break; } case StructuredCloneFile::eWasmBytecode: case StructuredCloneFile::eWasmCompiled: { MOZ_ASSERT(file.type() == DatabaseOrMutableFile::TPBackgroundIDBDatabaseFileParent); storedFileInfo->mFileActor = static_cast<DatabaseFile*>( file.get_PBackgroundIDBDatabaseFileParent()); MOZ_ASSERT(storedFileInfo->mFileActor); storedFileInfo->mFileInfo = storedFileInfo->mFileActor->GetFileInfo(); MOZ_ASSERT(storedFileInfo->mFileInfo); storedFileInfo->mType = fileAddInfo.type(); break; } default: MOZ_CRASH("Should never get here!"); } } } if (mDataOverThreshold) { StoredFileInfo* storedFileInfo = mStoredFileInfos.AppendElement(fallible); MOZ_ASSERT(storedFileInfo); RefPtr<FileManager> fileManager = aTransaction->GetDatabase()->GetFileManager(); MOZ_ASSERT(fileManager); storedFileInfo->mFileInfo = fileManager->GetNewFileInfo(); storedFileInfo->mInputStream = new SCInputStream(mParams.cloneInfo().data().data); storedFileInfo->mType = StructuredCloneFile::eStructuredClone; } return true; } nsresult ObjectStoreAddOrPutRequestOp::DoDatabaseWork(DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(aConnection->GetStorageConnection()); PROFILER_LABEL("IndexedDB", "ObjectStoreAddOrPutRequestOp::DoDatabaseWork", js::ProfileEntry::Category::STORAGE); if (NS_WARN_IF(IndexedDatabaseManager::InLowDiskSpaceMode())) { return NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR; } DatabaseConnection::AutoSavepoint autoSave; nsresult rv = autoSave.Start(Transaction()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } bool objectStoreHasIndexes; rv = ObjectStoreHasIndexes(this, aConnection, mParams.objectStoreId(), mObjectStoreMayHaveIndexes, &objectStoreHasIndexes); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // This will be the final key we use. Key& key = mResponse; key = mParams.key(); const bool keyUnset = key.IsUnset(); const int64_t osid = mParams.objectStoreId(); // First delete old index_data_values if we're overwriting something and we // have indexes. if (mOverwrite && !keyUnset && objectStoreHasIndexes) { rv = RemoveOldIndexDataValues(aConnection); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } // 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. DatabaseConnection::CachedStatement stmt; if (!mOverwrite || keyUnset) { rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( "INSERT INTO object_data " "(object_store_id, key, file_ids, data) " "VALUES (:osid, :key, :file_ids, :data);"), &stmt); } else { rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( "INSERT OR REPLACE INTO object_data " "(object_store_id, key, file_ids, data) " "VALUES (:osid, :key, :file_ids, :data);"), &stmt); } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("osid"), osid); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } const SerializedStructuredCloneWriteInfo& cloneInfo = mParams.cloneInfo(); const JSStructuredCloneData& cloneData = cloneInfo.data().data; size_t cloneDataSize = cloneData.Size(); MOZ_ASSERT(!keyUnset || mMetadata->mCommonMetadata.autoIncrement(), "Should have key unless autoIncrement"); int64_t autoIncrementNum = 0; if (mMetadata->mCommonMetadata.autoIncrement()) { if (keyUnset) { autoIncrementNum = mMetadata->mNextAutoIncrementId; MOZ_ASSERT(autoIncrementNum > 0); if (autoIncrementNum > (1LL << 53)) { return NS_ERROR_DOM_INDEXEDDB_CONSTRAINT_ERR; } key.SetFromInteger(autoIncrementNum); } else if (key.IsFloat() && key.ToFloat() >= mMetadata->mNextAutoIncrementId) { autoIncrementNum = floor(key.ToFloat()); } 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<double>(autoIncrementNum)); static const size_t keyPropSize = sizeof(uint64_t); char keyPropBuffer[keyPropSize]; LittleEndian::writeUint64(keyPropBuffer, keyPropValue); auto iter = cloneData.Iter(); DebugOnly<bool> result = iter.AdvanceAcrossSegments(cloneData, cloneInfo.offsetToKeyProp()); MOZ_ASSERT(result); for (uint32_t index = 0; index < keyPropSize; index++) { char* keyPropPointer = iter.Data(); *keyPropPointer = keyPropBuffer[index]; result = iter.AdvanceAcrossSegments(cloneData, 1); MOZ_ASSERT(result); } } } key.BindToStatement(stmt, NS_LITERAL_CSTRING("key")); 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; uint32_t index = mStoredFileInfos.Length() - 1; int64_t data = (uint64_t(flags) << 32) | index; rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("data"), data); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } else { nsCString flatCloneData; flatCloneData.SetLength(cloneDataSize); auto iter = cloneData.Iter(); cloneData.ReadBytes(iter, flatCloneData.BeginWriting(), cloneDataSize); // Compress the bytes before adding into the database. const char* uncompressed = flatCloneData.BeginReading(); size_t uncompressedLength = cloneDataSize; size_t compressedLength = snappy::MaxCompressedLength(uncompressedLength); UniqueFreePtr<char> compressed( static_cast<char*>(malloc(compressedLength))); if (NS_WARN_IF(!compressed)) { return NS_ERROR_OUT_OF_MEMORY; } snappy::RawCompress(uncompressed, uncompressedLength, compressed.get(), &compressedLength); uint8_t* dataBuffer = reinterpret_cast<uint8_t*>(compressed.release()); size_t dataBufferLength = compressedLength; rv = stmt->BindAdoptedBlobByName(NS_LITERAL_CSTRING("data"), dataBuffer, dataBufferLength); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } 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> fileHelper; nsAutoString fileIds; for (uint32_t count = mStoredFileInfos.Length(), index = 0; index < count; index++) { StoredFileInfo& storedFileInfo = mStoredFileInfos[index]; MOZ_ASSERT(storedFileInfo.mFileInfo); // If there is a StoredFileInfo, then one of the following is true: // - This was an overflow structured clone and storedFileInfo.mInputStream // MUST be non-null. // - This is a reference to a Blob that may or may not have already been // written to disk. storedFileInfo.mFileActor MUST be non-null, but // its GetBlockingInputStream may return null (so don't assert on them). // - It's a mutable file. No writing will be performed. MOZ_ASSERT(storedFileInfo.mInputStream || storedFileInfo.mFileActor || storedFileInfo.mType == StructuredCloneFile::eMutableFile); nsCOMPtr<nsIInputStream> inputStream; // Check for an explicit stream, like a structured clone stream. storedFileInfo.mInputStream.swap(inputStream); // Check for a blob-backed stream otherwise. if (!inputStream && storedFileInfo.mFileActor) { ErrorResult streamRv; inputStream = storedFileInfo.mFileActor->GetBlockingInputStream(streamRv); if (NS_WARN_IF(streamRv.Failed())) { return streamRv.StealNSResult(); } } if (inputStream) { if (fileHelper.isNothing()) { RefPtr<FileManager> fileManager = Transaction()->GetDatabase()->GetFileManager(); MOZ_ASSERT(fileManager); fileHelper.emplace(fileManager); rv = fileHelper->Init(); if (NS_WARN_IF(NS_FAILED(rv))) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } } RefPtr<FileInfo>& fileInfo = storedFileInfo.mFileInfo; nsCOMPtr<nsIFile> file = fileHelper->GetFile(fileInfo); if (NS_WARN_IF(!file)) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } nsCOMPtr<nsIFile> journalFile = fileHelper->GetJournalFile(fileInfo); if (NS_WARN_IF(!journalFile)) { IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } bool compress = storedFileInfo.mType == StructuredCloneFile::eStructuredClone; rv = fileHelper->CreateFileFromStream(file, journalFile, inputStream, compress); if (NS_FAILED(rv) && NS_ERROR_GET_MODULE(rv) != NS_ERROR_MODULE_DOM_INDEXEDDB) { IDB_REPORT_INTERNAL_ERR(); rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } if (NS_WARN_IF(NS_FAILED(rv))) { // Try to remove the file if the copy failed. nsresult rv2 = fileHelper->RemoveFile(file, journalFile); if (NS_WARN_IF(NS_FAILED(rv2))) { return rv; } return rv; } if (storedFileInfo.mFileActor) { storedFileInfo.mFileActor->WriteSucceededClearBlobImpl(); } } if (index) { fileIds.Append(' '); } storedFileInfo.Serialize(fileIds); } rv = stmt->BindStringByName(NS_LITERAL_CSTRING("file_ids"), fileIds); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } else { rv = stmt->BindNullByName(NS_LITERAL_CSTRING("file_ids")); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } rv = stmt->Execute(); if (rv == NS_ERROR_STORAGE_CONSTRAINT) { MOZ_ASSERT(!keyUnset, "Generated key had a collision!"); return rv; } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Update our indexes if needed. if (!mParams.indexUpdateInfos().IsEmpty()) { MOZ_ASSERT(mUniqueIndexTable.isSome()); // Write the index_data_values column. AutoTArray<IndexDataValue, 32> indexValues; rv = IndexDataValuesFromUpdateInfos(mParams.indexUpdateInfos(), mUniqueIndexTable.ref(), indexValues); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = UpdateIndexValues(aConnection, osid, key, indexValues); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = InsertIndexTableRows(aConnection, osid, key, indexValues); if (NS_FAILED(rv)) { return rv; } } rv = autoSave.Commit(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (autoIncrementNum) { mMetadata->mNextAutoIncrementId = autoIncrementNum + 1; Transaction()->NoteModifiedAutoIncrementObjectStore(mMetadata); } return NS_OK; } void ObjectStoreAddOrPutRequestOp::GetResponse(RequestResponse& aResponse) { AssertIsOnOwningThread(); if (mOverwrite) { aResponse = ObjectStorePutResponse(mResponse); } else { aResponse = ObjectStoreAddResponse(mResponse); } } 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::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; mIter.Advance(mData, count); } return NS_OK; } NS_IMETHODIMP ObjectStoreAddOrPutRequestOp:: SCInputStream::IsNonBlocking(bool* _retval) { *_retval = false; return NS_OK; } ObjectStoreGetRequestOp::ObjectStoreGetRequestOp(TransactionBase* aTransaction, const RequestParams& aParams, bool aGetAll) : NormalTransactionOp(aTransaction) , mObjectStoreId(aGetAll ? aParams.get_ObjectStoreGetAllParams().objectStoreId() : aParams.get_ObjectStoreGetParams().objectStoreId()) , mDatabase(aTransaction->GetDatabase()) , mOptionalKeyRange(aGetAll ? aParams.get_ObjectStoreGetAllParams() .optionalKeyRange() : OptionalKeyRange(aParams.get_ObjectStoreGetParams() .keyRange())) , mBackgroundParent(aTransaction->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.type() == OptionalKeyRange::TSerializedKeyRange); MOZ_ASSERT(mBackgroundParent); } template <typename T> void MoveData(StructuredCloneReadInfo& aInfo, T& aResult); template <> void MoveData<SerializedStructuredCloneReadInfo>( StructuredCloneReadInfo& aInfo, SerializedStructuredCloneReadInfo& aResult) { aResult.data().data = Move(aInfo.mData); aResult.hasPreprocessInfo() = aInfo.mHasPreprocessInfo; } template <> void MoveData<WasmModulePreprocessInfo>(StructuredCloneReadInfo& aInfo, WasmModulePreprocessInfo& aResult) { } template <bool aForPreprocess, typename T> nsresult ObjectStoreGetRequestOp::ConvertResponse(StructuredCloneReadInfo& aInfo, T& aResult) { MoveData(aInfo, aResult); FallibleTArray<SerializedStructuredCloneFile> serializedFiles; nsresult rv = SerializeStructuredCloneFiles(mBackgroundParent, mDatabase, aInfo.mFiles, aForPreprocess, serializedFiles); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(aResult.files().IsEmpty()); aResult.files().SwapElements(serializedFiles); return NS_OK; } nsresult ObjectStoreGetRequestOp::DoDatabaseWork(DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); MOZ_ASSERT_IF(!mGetAll, mOptionalKeyRange.type() == OptionalKeyRange::TSerializedKeyRange); MOZ_ASSERT_IF(!mGetAll, mLimit == 1); PROFILER_LABEL("IndexedDB", "ObjectStoreGetRequestOp::DoDatabaseWork", js::ProfileEntry::Category::STORAGE); const bool hasKeyRange = mOptionalKeyRange.type() == OptionalKeyRange::TSerializedKeyRange; nsAutoCString keyRangeClause; if (hasKeyRange) { GetBindingClauseForKeyRange(mOptionalKeyRange.get_SerializedKeyRange(), NS_LITERAL_CSTRING("key"), keyRangeClause); } nsCString limitClause; if (mLimit) { limitClause.AssignLiteral(" LIMIT "); limitClause.AppendInt(mLimit); } nsCString query = NS_LITERAL_CSTRING("SELECT file_ids, data " "FROM object_data " "WHERE object_store_id = :osid") + keyRangeClause + NS_LITERAL_CSTRING(" ORDER BY key ASC") + limitClause; DatabaseConnection::CachedStatement stmt; nsresult rv = aConnection->GetCachedStatement(query, &stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("osid"), mObjectStoreId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (hasKeyRange) { rv = BindKeyRangeToStatement(mOptionalKeyRange.get_SerializedKeyRange(), stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } bool hasResult; while (NS_SUCCEEDED((rv = stmt->ExecuteStep(&hasResult))) && hasResult) { StructuredCloneReadInfo* cloneInfo = mResponse.AppendElement(fallible); if (NS_WARN_IF(!cloneInfo)) { return NS_ERROR_OUT_OF_MEMORY; } rv = GetStructuredCloneReadInfoFromStatement(stmt, 1, 0, mDatabase->GetFileManager(), cloneInfo); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (cloneInfo->mHasPreprocessInfo) { mPreprocessInfoCount++; } } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT_IF(!mGetAll, mResponse.Length() <= 1); return NS_OK; } bool ObjectStoreGetRequestOp::HasPreprocessInfo() { return mPreprocessInfoCount > 0; } nsresult ObjectStoreGetRequestOp::GetPreprocessParams(PreprocessParams& aParams) { AssertIsOnOwningThread(); MOZ_ASSERT(!mResponse.IsEmpty()); if (mGetAll) { aParams = ObjectStoreGetAllPreprocessParams(); FallibleTArray<WasmModulePreprocessInfo> falliblePreprocessInfos; if (NS_WARN_IF(!falliblePreprocessInfos.SetLength(mPreprocessInfoCount, fallible))) { return NS_ERROR_OUT_OF_MEMORY; } uint32_t fallibleIndex = 0; for (uint32_t count = mResponse.Length(), index = 0; index < count; index++) { StructuredCloneReadInfo& info = mResponse[index]; if (info.mHasPreprocessInfo) { nsresult rv = ConvertResponse<true>(info, falliblePreprocessInfos[fallibleIndex++]); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } } nsTArray<WasmModulePreprocessInfo>& preprocessInfos = aParams.get_ObjectStoreGetAllPreprocessParams().preprocessInfos(); falliblePreprocessInfos.SwapElements(preprocessInfos); return NS_OK; } aParams = ObjectStoreGetPreprocessParams(); WasmModulePreprocessInfo& preprocessInfo = aParams.get_ObjectStoreGetPreprocessParams().preprocessInfo(); nsresult rv = ConvertResponse<true>(mResponse[0], preprocessInfo); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } void ObjectStoreGetRequestOp::GetResponse(RequestResponse& aResponse) { MOZ_ASSERT_IF(mLimit, mResponse.Length() <= mLimit); if (mGetAll) { aResponse = ObjectStoreGetAllResponse(); if (!mResponse.IsEmpty()) { FallibleTArray<SerializedStructuredCloneReadInfo> fallibleCloneInfos; if (NS_WARN_IF(!fallibleCloneInfos.SetLength(mResponse.Length(), fallible))) { aResponse = NS_ERROR_OUT_OF_MEMORY; return; } for (uint32_t count = mResponse.Length(), index = 0; index < count; index++) { nsresult rv = ConvertResponse<false>(mResponse[index], fallibleCloneInfos[index]); if (NS_WARN_IF(NS_FAILED(rv))) { aResponse = rv; return; } } nsTArray<SerializedStructuredCloneReadInfo>& cloneInfos = aResponse.get_ObjectStoreGetAllResponse().cloneInfos(); fallibleCloneInfos.SwapElements(cloneInfos); } return; } aResponse = ObjectStoreGetResponse(); if (!mResponse.IsEmpty()) { SerializedStructuredCloneReadInfo& serializedInfo = aResponse.get_ObjectStoreGetResponse().cloneInfo(); nsresult rv = ConvertResponse<false>(mResponse[0], serializedInfo); if (NS_WARN_IF(NS_FAILED(rv))) { aResponse = rv; } } } ObjectStoreGetKeyRequestOp::ObjectStoreGetKeyRequestOp( TransactionBase* aTransaction, const RequestParams& aParams, bool aGetAll) : NormalTransactionOp(aTransaction) , mObjectStoreId(aGetAll ? aParams.get_ObjectStoreGetAllKeysParams().objectStoreId() : aParams.get_ObjectStoreGetKeyParams().objectStoreId()) , mOptionalKeyRange(aGetAll ? aParams.get_ObjectStoreGetAllKeysParams() .optionalKeyRange() : OptionalKeyRange(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.type() == OptionalKeyRange::TSerializedKeyRange); } nsresult ObjectStoreGetKeyRequestOp::DoDatabaseWork(DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); PROFILER_LABEL("IndexedDB", "ObjectStoreGetKeyRequestOp::DoDatabaseWork", js::ProfileEntry::Category::STORAGE); const bool hasKeyRange = mOptionalKeyRange.type() == OptionalKeyRange::TSerializedKeyRange; nsAutoCString keyRangeClause; if (hasKeyRange) { GetBindingClauseForKeyRange(mOptionalKeyRange.get_SerializedKeyRange(), NS_LITERAL_CSTRING("key"), keyRangeClause); } nsAutoCString limitClause; if (mLimit) { limitClause.AssignLiteral(" LIMIT "); limitClause.AppendInt(mLimit); } nsCString query = NS_LITERAL_CSTRING("SELECT key " "FROM object_data " "WHERE object_store_id = :osid") + keyRangeClause + NS_LITERAL_CSTRING(" ORDER BY key ASC") + limitClause; DatabaseConnection::CachedStatement stmt; nsresult rv = aConnection->GetCachedStatement(query, &stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("osid"), mObjectStoreId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (hasKeyRange) { rv = BindKeyRangeToStatement(mOptionalKeyRange.get_SerializedKeyRange(), stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } bool hasResult; while(NS_SUCCEEDED((rv = stmt->ExecuteStep(&hasResult))) && hasResult) { Key* key = mResponse.AppendElement(fallible); if (NS_WARN_IF(!key)) { return NS_ERROR_OUT_OF_MEMORY; } rv = key->SetFromStatement(stmt, 0); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT_IF(!mGetAll, mResponse.Length() <= 1); return NS_OK; } void ObjectStoreGetKeyRequestOp::GetResponse(RequestResponse& aResponse) { MOZ_ASSERT_IF(mLimit, mResponse.Length() <= mLimit); if (mGetAll) { aResponse = ObjectStoreGetAllKeysResponse(); if (!mResponse.IsEmpty()) { nsTArray<Key>& response = aResponse.get_ObjectStoreGetAllKeysResponse().keys(); mResponse.SwapElements(response); } return; } aResponse = ObjectStoreGetKeyResponse(); if (!mResponse.IsEmpty()) { aResponse.get_ObjectStoreGetKeyResponse().key() = Move(mResponse[0]); } } ObjectStoreDeleteRequestOp::ObjectStoreDeleteRequestOp( TransactionBase* aTransaction, const ObjectStoreDeleteParams& aParams) : NormalTransactionOp(aTransaction) , mParams(aParams) , mObjectStoreMayHaveIndexes(false) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aTransaction); RefPtr<FullObjectStoreMetadata> metadata = aTransaction->GetMetadataForObjectStoreId(mParams.objectStoreId()); MOZ_ASSERT(metadata); mObjectStoreMayHaveIndexes = metadata->HasLiveIndexes(); } nsresult ObjectStoreDeleteRequestOp::DoDatabaseWork(DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); PROFILER_LABEL("IndexedDB", "ObjectStoreDeleteRequestOp::DoDatabaseWork", js::ProfileEntry::Category::STORAGE); DatabaseConnection::AutoSavepoint autoSave; nsresult rv = autoSave.Start(Transaction()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } bool objectStoreHasIndexes; rv = ObjectStoreHasIndexes(this, aConnection, mParams.objectStoreId(), mObjectStoreMayHaveIndexes, &objectStoreHasIndexes); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (objectStoreHasIndexes) { rv = DeleteObjectStoreDataTableRowsWithIndexes(aConnection, mParams.objectStoreId(), mParams.keyRange()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } else { NS_NAMED_LITERAL_CSTRING(objectStoreIdString, "object_store_id"); nsAutoCString keyRangeClause; GetBindingClauseForKeyRange(mParams.keyRange(), NS_LITERAL_CSTRING("key"), keyRangeClause); DatabaseConnection::CachedStatement stmt; rv = aConnection->GetCachedStatement( NS_LITERAL_CSTRING("DELETE FROM object_data " "WHERE object_store_id = :") + objectStoreIdString + keyRangeClause + NS_LITERAL_CSTRING(";"), &stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->BindInt64ByName(objectStoreIdString, mParams.objectStoreId()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = BindKeyRangeToStatement(mParams.keyRange(), stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->Execute(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } rv = autoSave.Commit(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } ObjectStoreClearRequestOp::ObjectStoreClearRequestOp( TransactionBase* aTransaction, const ObjectStoreClearParams& aParams) : NormalTransactionOp(aTransaction) , mParams(aParams) , mObjectStoreMayHaveIndexes(false) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aTransaction); RefPtr<FullObjectStoreMetadata> metadata = aTransaction->GetMetadataForObjectStoreId(mParams.objectStoreId()); MOZ_ASSERT(metadata); mObjectStoreMayHaveIndexes = metadata->HasLiveIndexes(); } nsresult ObjectStoreClearRequestOp::DoDatabaseWork(DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); PROFILER_LABEL("IndexedDB", "ObjectStoreClearRequestOp::DoDatabaseWork", js::ProfileEntry::Category::STORAGE); DatabaseConnection::AutoSavepoint autoSave; nsresult rv = autoSave.Start(Transaction()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } bool objectStoreHasIndexes; rv = ObjectStoreHasIndexes(this, aConnection, mParams.objectStoreId(), mObjectStoreMayHaveIndexes, &objectStoreHasIndexes); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (objectStoreHasIndexes) { rv = DeleteObjectStoreDataTableRowsWithIndexes(aConnection, mParams.objectStoreId(), void_t()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } else { DatabaseConnection::CachedStatement stmt; rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( "DELETE FROM object_data " "WHERE object_store_id = :object_store_id;"), &stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("object_store_id"), mParams.objectStoreId()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->Execute(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } rv = autoSave.Commit(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } nsresult ObjectStoreCountRequestOp::DoDatabaseWork(DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); PROFILER_LABEL("IndexedDB", "ObjectStoreCountRequestOp::DoDatabaseWork", js::ProfileEntry::Category::STORAGE); const bool hasKeyRange = mParams.optionalKeyRange().type() == OptionalKeyRange::TSerializedKeyRange; nsAutoCString keyRangeClause; if (hasKeyRange) { GetBindingClauseForKeyRange( mParams.optionalKeyRange().get_SerializedKeyRange(), NS_LITERAL_CSTRING("key"), keyRangeClause); } nsCString query = NS_LITERAL_CSTRING("SELECT count(*) " "FROM object_data " "WHERE object_store_id = :osid") + keyRangeClause; DatabaseConnection::CachedStatement stmt; nsresult rv = aConnection->GetCachedStatement(query, &stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("osid"), mParams.objectStoreId()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (hasKeyRange) { rv = BindKeyRangeToStatement( mParams.optionalKeyRange().get_SerializedKeyRange(), stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } bool hasResult; rv = stmt->ExecuteStep(&hasResult); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (NS_WARN_IF(!hasResult)) { MOZ_ASSERT(false, "This should never be possible!"); IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } int64_t count = stmt->AsInt64(0); if (NS_WARN_IF(count < 0)) { MOZ_ASSERT(false, "This should never be possible!"); IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } mResponse.count() = count; return NS_OK; } // static already_AddRefed<FullIndexMetadata> IndexRequestOpBase::IndexMetadataForParams(TransactionBase* aTransaction, const RequestParams& aParams) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aTransaction); MOZ_ASSERT(aParams.type() == RequestParams::TIndexGetParams || aParams.type() == RequestParams::TIndexGetKeyParams || aParams.type() == RequestParams::TIndexGetAllParams || aParams.type() == RequestParams::TIndexGetAllKeysParams || aParams.type() == RequestParams::TIndexCountParams); uint64_t objectStoreId; uint64_t 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 RefPtr<FullObjectStoreMetadata> objectStoreMetadata = aTransaction->GetMetadataForObjectStoreId(objectStoreId); MOZ_ASSERT(objectStoreMetadata); RefPtr<FullIndexMetadata> indexMetadata = aTransaction->GetMetadataForIndexId(objectStoreMetadata, indexId); MOZ_ASSERT(indexMetadata); return indexMetadata.forget(); } IndexGetRequestOp::IndexGetRequestOp(TransactionBase* aTransaction, const RequestParams& aParams, bool aGetAll) : IndexRequestOpBase(aTransaction, aParams) , mDatabase(aTransaction->GetDatabase()) , mOptionalKeyRange(aGetAll ? aParams.get_IndexGetAllParams().optionalKeyRange() : OptionalKeyRange(aParams.get_IndexGetParams() .keyRange())) , mBackgroundParent(aTransaction->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.type() == OptionalKeyRange::TSerializedKeyRange); MOZ_ASSERT(mBackgroundParent); } nsresult IndexGetRequestOp::DoDatabaseWork(DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); MOZ_ASSERT_IF(!mGetAll, mOptionalKeyRange.type() == OptionalKeyRange::TSerializedKeyRange); MOZ_ASSERT_IF(!mGetAll, mLimit == 1); PROFILER_LABEL("IndexedDB", "IndexGetRequestOp::DoDatabaseWork", js::ProfileEntry::Category::STORAGE); const bool hasKeyRange = mOptionalKeyRange.type() == OptionalKeyRange::TSerializedKeyRange; nsCString indexTable; if (mMetadata->mCommonMetadata.unique()) { indexTable.AssignLiteral("unique_index_data "); } else { indexTable.AssignLiteral("index_data "); } nsAutoCString keyRangeClause; if (hasKeyRange) { GetBindingClauseForKeyRange(mOptionalKeyRange.get_SerializedKeyRange(), NS_LITERAL_CSTRING("value"), keyRangeClause); } nsCString limitClause; if (mLimit) { limitClause.AssignLiteral(" LIMIT "); limitClause.AppendInt(mLimit); } nsCString query = NS_LITERAL_CSTRING("SELECT file_ids, data " "FROM object_data " "INNER JOIN ") + indexTable + NS_LITERAL_CSTRING("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 = :index_id") + keyRangeClause + limitClause; DatabaseConnection::CachedStatement stmt; nsresult rv = aConnection->GetCachedStatement(query, &stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("index_id"), mMetadata->mCommonMetadata.id()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (hasKeyRange) { rv = BindKeyRangeToStatement(mOptionalKeyRange.get_SerializedKeyRange(), stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } bool hasResult; while(NS_SUCCEEDED((rv = stmt->ExecuteStep(&hasResult))) && hasResult) { StructuredCloneReadInfo* cloneInfo = mResponse.AppendElement(fallible); if (NS_WARN_IF(!cloneInfo)) { return NS_ERROR_OUT_OF_MEMORY; } rv = GetStructuredCloneReadInfoFromStatement(stmt, 1, 0, mDatabase->GetFileManager(), cloneInfo); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (cloneInfo->mHasPreprocessInfo) { IDB_WARNING("Preprocessing for indexes not yet implemented!"); return NS_ERROR_NOT_IMPLEMENTED; } } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT_IF(!mGetAll, mResponse.Length() <= 1); return NS_OK; } void IndexGetRequestOp::GetResponse(RequestResponse& aResponse) { MOZ_ASSERT_IF(!mGetAll, mResponse.Length() <= 1); if (mGetAll) { aResponse = IndexGetAllResponse(); if (!mResponse.IsEmpty()) { FallibleTArray<SerializedStructuredCloneReadInfo> fallibleCloneInfos; if (NS_WARN_IF(!fallibleCloneInfos.SetLength(mResponse.Length(), fallible))) { aResponse = NS_ERROR_OUT_OF_MEMORY; return; } for (uint32_t count = mResponse.Length(), index = 0; index < count; index++) { StructuredCloneReadInfo& info = mResponse[index]; SerializedStructuredCloneReadInfo& serializedInfo = fallibleCloneInfos[index]; serializedInfo.data().data = Move(info.mData); FallibleTArray<SerializedStructuredCloneFile> serializedFiles; nsresult rv = SerializeStructuredCloneFiles(mBackgroundParent, mDatabase, info.mFiles, /* aForPreprocess */ false, serializedFiles); if (NS_WARN_IF(NS_FAILED(rv))) { aResponse = rv; return; } MOZ_ASSERT(serializedInfo.files().IsEmpty()); serializedInfo.files().SwapElements(serializedFiles); } nsTArray<SerializedStructuredCloneReadInfo>& cloneInfos = aResponse.get_IndexGetAllResponse().cloneInfos(); fallibleCloneInfos.SwapElements(cloneInfos); } return; } aResponse = IndexGetResponse(); if (!mResponse.IsEmpty()) { StructuredCloneReadInfo& info = mResponse[0]; SerializedStructuredCloneReadInfo& serializedInfo = aResponse.get_IndexGetResponse().cloneInfo(); serializedInfo.data().data = Move(info.mData); FallibleTArray<SerializedStructuredCloneFile> serializedFiles; nsresult rv = SerializeStructuredCloneFiles(mBackgroundParent, mDatabase, info.mFiles, /* aForPreprocess */ false, serializedFiles); if (NS_WARN_IF(NS_FAILED(rv))) { aResponse = rv; return; } MOZ_ASSERT(serializedInfo.files().IsEmpty()); serializedInfo.files().SwapElements(serializedFiles); } } IndexGetKeyRequestOp::IndexGetKeyRequestOp(TransactionBase* aTransaction, const RequestParams& aParams, bool aGetAll) : IndexRequestOpBase(aTransaction, aParams) , mOptionalKeyRange(aGetAll ? aParams.get_IndexGetAllKeysParams().optionalKeyRange() : OptionalKeyRange(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.type() == OptionalKeyRange::TSerializedKeyRange); } nsresult IndexGetKeyRequestOp::DoDatabaseWork(DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); MOZ_ASSERT_IF(!mGetAll, mOptionalKeyRange.type() == OptionalKeyRange::TSerializedKeyRange); MOZ_ASSERT_IF(!mGetAll, mLimit == 1); PROFILER_LABEL("IndexedDB", "IndexGetKeyRequestOp::DoDatabaseWork", js::ProfileEntry::Category::STORAGE); const bool hasKeyRange = mOptionalKeyRange.type() == OptionalKeyRange::TSerializedKeyRange; nsCString indexTable; if (mMetadata->mCommonMetadata.unique()) { indexTable.AssignLiteral("unique_index_data "); } else { indexTable.AssignLiteral("index_data "); } nsAutoCString keyRangeClause; if (hasKeyRange) { GetBindingClauseForKeyRange(mOptionalKeyRange.get_SerializedKeyRange(), NS_LITERAL_CSTRING("value"), keyRangeClause); } nsCString limitClause; if (mLimit) { limitClause.AssignLiteral(" LIMIT "); limitClause.AppendInt(mLimit); } nsCString query = NS_LITERAL_CSTRING("SELECT object_data_key " "FROM ") + indexTable + NS_LITERAL_CSTRING("WHERE index_id = :index_id") + keyRangeClause + limitClause; DatabaseConnection::CachedStatement stmt; nsresult rv = aConnection->GetCachedStatement(query, &stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("index_id"), mMetadata->mCommonMetadata.id()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (hasKeyRange) { rv = BindKeyRangeToStatement(mOptionalKeyRange.get_SerializedKeyRange(), stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } bool hasResult; while(NS_SUCCEEDED((rv = stmt->ExecuteStep(&hasResult))) && hasResult) { Key* key = mResponse.AppendElement(fallible); if (NS_WARN_IF(!key)) { return NS_ERROR_OUT_OF_MEMORY; } rv = key->SetFromStatement(stmt, 0); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT_IF(!mGetAll, mResponse.Length() <= 1); return NS_OK; } void IndexGetKeyRequestOp::GetResponse(RequestResponse& aResponse) { MOZ_ASSERT_IF(!mGetAll, mResponse.Length() <= 1); if (mGetAll) { aResponse = IndexGetAllKeysResponse(); if (!mResponse.IsEmpty()) { mResponse.SwapElements(aResponse.get_IndexGetAllKeysResponse().keys()); } return; } aResponse = IndexGetKeyResponse(); if (!mResponse.IsEmpty()) { aResponse.get_IndexGetKeyResponse().key() = Move(mResponse[0]); } } nsresult IndexCountRequestOp::DoDatabaseWork(DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); PROFILER_LABEL("IndexedDB", "IndexCountRequestOp::DoDatabaseWork", js::ProfileEntry::Category::STORAGE); const bool hasKeyRange = mParams.optionalKeyRange().type() == OptionalKeyRange::TSerializedKeyRange; nsCString indexTable; if (mMetadata->mCommonMetadata.unique()) { indexTable.AssignLiteral("unique_index_data "); } else { indexTable.AssignLiteral("index_data "); } nsAutoCString keyRangeClause; if (hasKeyRange) { GetBindingClauseForKeyRange( mParams.optionalKeyRange().get_SerializedKeyRange(), NS_LITERAL_CSTRING("value"), keyRangeClause); } nsCString query = NS_LITERAL_CSTRING("SELECT count(*) " "FROM ") + indexTable + NS_LITERAL_CSTRING("WHERE index_id = :index_id") + keyRangeClause; DatabaseConnection::CachedStatement stmt; nsresult rv = aConnection->GetCachedStatement(query, &stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("index_id"), mMetadata->mCommonMetadata.id()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (hasKeyRange) { rv = BindKeyRangeToStatement( mParams.optionalKeyRange().get_SerializedKeyRange(), stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } bool hasResult; rv = stmt->ExecuteStep(&hasResult); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (NS_WARN_IF(!hasResult)) { MOZ_ASSERT(false, "This should never be possible!"); IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } int64_t count = stmt->AsInt64(0); if (NS_WARN_IF(count < 0)) { MOZ_ASSERT(false, "This should never be possible!"); IDB_REPORT_INTERNAL_ERR(); return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } mResponse.count() = count; return NS_OK; } 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. We clear the retrieved files to prevent // the assertion failure in SendResponseInternal when mResponse.type() is // CursorResponse::Tnsresult. if (Transaction()->IsInvalidated() && !mFiles.IsEmpty()) { mFiles.Clear(); } mCursor->SendResponseInternal(mResponse, mFiles); } #ifdef DEBUG mResponseSent = true; #endif return false; } 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(); } nsresult Cursor:: CursorOpBase::PopulateResponseFromStatement( DatabaseConnection::CachedStatement& aStmt, bool aInitializeResponse) { Transaction()->AssertIsOnConnectionThread(); MOZ_ASSERT(mResponse.type() == CursorResponse::T__None); MOZ_ASSERT_IF(mFiles.IsEmpty(), aInitializeResponse); nsresult rv = mCursor->mKey.SetFromStatement(aStmt, 0); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } switch (mCursor->mType) { case OpenCursorParams::TObjectStoreOpenCursorParams: { StructuredCloneReadInfo cloneInfo; rv = GetStructuredCloneReadInfoFromStatement(aStmt, 2, 1, mCursor->mFileManager, &cloneInfo); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (cloneInfo.mHasPreprocessInfo) { IDB_WARNING("Preprocessing for cursors not yet implemented!"); return NS_ERROR_NOT_IMPLEMENTED; } if (aInitializeResponse) { mResponse = nsTArray<ObjectStoreCursorResponse>(); } else { MOZ_ASSERT(mResponse.type() == CursorResponse::TArrayOfObjectStoreCursorResponse); } auto& responses = mResponse.get_ArrayOfObjectStoreCursorResponse(); auto& response = *responses.AppendElement(); response.cloneInfo().data().data = Move(cloneInfo.mData); response.key() = mCursor->mKey; mFiles.AppendElement(Move(cloneInfo.mFiles)); break; } case OpenCursorParams::TObjectStoreOpenKeyCursorParams: { MOZ_ASSERT(aInitializeResponse); mResponse = ObjectStoreKeyCursorResponse(mCursor->mKey); break; } case OpenCursorParams::TIndexOpenCursorParams: { rv = mCursor->mSortKey.SetFromStatement(aStmt, 1); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = mCursor->mObjectKey.SetFromStatement(aStmt, 2); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } StructuredCloneReadInfo cloneInfo; rv = GetStructuredCloneReadInfoFromStatement(aStmt, 4, 3, mCursor->mFileManager, &cloneInfo); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (cloneInfo.mHasPreprocessInfo) { IDB_WARNING("Preprocessing for cursors not yet implemented!"); return NS_ERROR_NOT_IMPLEMENTED; } MOZ_ASSERT(aInitializeResponse); mResponse = IndexCursorResponse(); auto& response = mResponse.get_IndexCursorResponse(); response.cloneInfo().data().data = Move(cloneInfo.mData); response.key() = mCursor->mKey; response.sortKey() = mCursor->mSortKey; response.objectKey() = mCursor->mObjectKey; mFiles.AppendElement(Move(cloneInfo.mFiles)); break; } case OpenCursorParams::TIndexOpenKeyCursorParams: { rv = mCursor->mSortKey.SetFromStatement(aStmt, 1); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = mCursor->mObjectKey.SetFromStatement(aStmt, 2); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(aInitializeResponse); mResponse = IndexKeyCursorResponse(mCursor->mKey, mCursor->mSortKey, mCursor->mObjectKey); break; } default: MOZ_CRASH("Should never get here!"); } return NS_OK; } void Cursor:: OpenOp::GetRangeKeyInfo(bool aLowerBound, Key* aKey, bool* aOpen) { AssertIsOnConnectionThread(); MOZ_ASSERT(aKey); MOZ_ASSERT(aKey->IsUnset()); MOZ_ASSERT(aOpen); if (mOptionalKeyRange.type() == OptionalKeyRange::TSerializedKeyRange) { const SerializedKeyRange& range = mOptionalKeyRange.get_SerializedKeyRange(); if (range.isOnly()) { *aKey = range.lower(); *aOpen = false; #ifdef ENABLE_INTL_API if (mCursor->IsLocaleAware()) { range.lower().ToLocaleBasedKey(*aKey, mCursor->mLocale); } #endif } else { *aKey = aLowerBound ? range.lower() : range.upper(); *aOpen = aLowerBound ? range.lowerOpen() : range.upperOpen(); #ifdef ENABLE_INTL_API if (mCursor->IsLocaleAware()) { if (aLowerBound) { range.lower().ToLocaleBasedKey(*aKey, mCursor->mLocale); } else { range.upper().ToLocaleBasedKey(*aKey, mCursor->mLocale); } } #endif } } else { *aOpen = false; } } nsresult Cursor:: OpenOp::DoObjectStoreDatabaseWork(DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(mCursor); MOZ_ASSERT(mCursor->mType == OpenCursorParams::TObjectStoreOpenCursorParams); MOZ_ASSERT(mCursor->mObjectStoreId); MOZ_ASSERT(mCursor->mFileManager); PROFILER_LABEL("IndexedDB", "Cursor::OpenOp::DoObjectStoreDatabaseWork", js::ProfileEntry::Category::STORAGE); const bool usingKeyRange = mOptionalKeyRange.type() == OptionalKeyRange::TSerializedKeyRange; NS_NAMED_LITERAL_CSTRING(keyString, "key"); NS_NAMED_LITERAL_CSTRING(id, "id"); NS_NAMED_LITERAL_CSTRING(openLimit, " LIMIT "); nsCString queryStart = NS_LITERAL_CSTRING("SELECT ") + keyString + NS_LITERAL_CSTRING(", file_ids, data " "FROM object_data " "WHERE object_store_id = :") + id; nsAutoCString keyRangeClause; if (usingKeyRange) { GetBindingClauseForKeyRange(mOptionalKeyRange.get_SerializedKeyRange(), keyString, keyRangeClause); } nsAutoCString directionClause = NS_LITERAL_CSTRING(" ORDER BY ") + keyString; switch (mCursor->mDirection) { case IDBCursor::NEXT: case IDBCursor::NEXT_UNIQUE: directionClause.AppendLiteral(" ASC"); break; case IDBCursor::PREV: case IDBCursor::PREV_UNIQUE: directionClause.AppendLiteral(" DESC"); 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. nsCString firstQuery = queryStart + keyRangeClause + directionClause + openLimit + NS_LITERAL_CSTRING("1"); DatabaseConnection::CachedStatement stmt; nsresult rv = aConnection->GetCachedStatement(firstQuery, &stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->BindInt64ByName(id, mCursor->mObjectStoreId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (usingKeyRange) { rv = BindKeyRangeToStatement(mOptionalKeyRange.get_SerializedKeyRange(), stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } bool hasResult; rv = stmt->ExecuteStep(&hasResult); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (!hasResult) { mResponse = void_t(); return NS_OK; } rv = PopulateResponseFromStatement(stmt, true); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Now we need to make the query to get the next match. keyRangeClause.Truncate(); nsAutoCString continueToKeyRangeClause; NS_NAMED_LITERAL_CSTRING(currentKey, "current_key"); NS_NAMED_LITERAL_CSTRING(rangeKey, "range_key"); switch (mCursor->mDirection) { case IDBCursor::NEXT: case IDBCursor::NEXT_UNIQUE: { Key upper; bool open; GetRangeKeyInfo(false, &upper, &open); AppendConditionClause(keyString, currentKey, false, false, keyRangeClause); AppendConditionClause(keyString, currentKey, false, true, continueToKeyRangeClause); if (usingKeyRange && !upper.IsUnset()) { AppendConditionClause(keyString, rangeKey, true, !open, keyRangeClause); AppendConditionClause(keyString, rangeKey, true, !open, continueToKeyRangeClause); mCursor->mRangeKey = upper; } break; } case IDBCursor::PREV: case IDBCursor::PREV_UNIQUE: { Key lower; bool open; GetRangeKeyInfo(true, &lower, &open); AppendConditionClause(keyString, currentKey, true, false, keyRangeClause); AppendConditionClause(keyString, currentKey, true, true, continueToKeyRangeClause); if (usingKeyRange && !lower.IsUnset()) { AppendConditionClause(keyString, rangeKey, false, !open, keyRangeClause); AppendConditionClause(keyString, rangeKey, false, !open, continueToKeyRangeClause); mCursor->mRangeKey = lower; } break; } default: MOZ_CRASH("Should never get here!"); } mCursor->mContinueQuery = queryStart + keyRangeClause + directionClause + openLimit; mCursor->mContinueToQuery = queryStart + continueToKeyRangeClause + directionClause + openLimit; return NS_OK; } nsresult Cursor:: OpenOp::DoObjectStoreKeyDatabaseWork(DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(mCursor); MOZ_ASSERT(mCursor->mType == OpenCursorParams::TObjectStoreOpenKeyCursorParams); MOZ_ASSERT(mCursor->mObjectStoreId); PROFILER_LABEL("IndexedDB", "Cursor::OpenOp::DoObjectStoreKeyDatabaseWork", js::ProfileEntry::Category::STORAGE); const bool usingKeyRange = mOptionalKeyRange.type() == OptionalKeyRange::TSerializedKeyRange; NS_NAMED_LITERAL_CSTRING(keyString, "key"); NS_NAMED_LITERAL_CSTRING(id, "id"); NS_NAMED_LITERAL_CSTRING(openLimit, " LIMIT "); nsCString queryStart = NS_LITERAL_CSTRING("SELECT ") + keyString + NS_LITERAL_CSTRING(" FROM object_data " "WHERE object_store_id = :") + id; nsAutoCString keyRangeClause; if (usingKeyRange) { GetBindingClauseForKeyRange(mOptionalKeyRange.get_SerializedKeyRange(), keyString, keyRangeClause); } nsAutoCString directionClause = NS_LITERAL_CSTRING(" ORDER BY ") + keyString; switch (mCursor->mDirection) { case IDBCursor::NEXT: case IDBCursor::NEXT_UNIQUE: directionClause.AppendLiteral(" ASC"); break; case IDBCursor::PREV: case IDBCursor::PREV_UNIQUE: directionClause.AppendLiteral(" DESC"); 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. nsCString firstQuery = queryStart + keyRangeClause + directionClause + openLimit + NS_LITERAL_CSTRING("1"); DatabaseConnection::CachedStatement stmt; nsresult rv = aConnection->GetCachedStatement(firstQuery, &stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->BindInt64ByName(id, mCursor->mObjectStoreId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (usingKeyRange) { rv = BindKeyRangeToStatement(mOptionalKeyRange.get_SerializedKeyRange(), stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } bool hasResult; rv = stmt->ExecuteStep(&hasResult); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (!hasResult) { mResponse = void_t(); return NS_OK; } rv = PopulateResponseFromStatement(stmt, true); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Now we need to make the query to get the next match. keyRangeClause.Truncate(); nsAutoCString continueToKeyRangeClause; NS_NAMED_LITERAL_CSTRING(currentKey, "current_key"); NS_NAMED_LITERAL_CSTRING(rangeKey, "range_key"); switch (mCursor->mDirection) { case IDBCursor::NEXT: case IDBCursor::NEXT_UNIQUE: { Key upper; bool open; GetRangeKeyInfo(false, &upper, &open); AppendConditionClause(keyString, currentKey, false, false, keyRangeClause); AppendConditionClause(keyString, currentKey, false, true, continueToKeyRangeClause); if (usingKeyRange && !upper.IsUnset()) { AppendConditionClause(keyString, rangeKey, true, !open, keyRangeClause); AppendConditionClause(keyString, rangeKey, true, !open, continueToKeyRangeClause); mCursor->mRangeKey = upper; } break; } case IDBCursor::PREV: case IDBCursor::PREV_UNIQUE: { Key lower; bool open; GetRangeKeyInfo(true, &lower, &open); AppendConditionClause(keyString, currentKey, true, false, keyRangeClause); AppendConditionClause(keyString, currentKey, true, true, continueToKeyRangeClause); if (usingKeyRange && !lower.IsUnset()) { AppendConditionClause(keyString, rangeKey, false, !open, keyRangeClause); AppendConditionClause(keyString, rangeKey, false, !open, continueToKeyRangeClause); mCursor->mRangeKey = lower; } break; } default: MOZ_CRASH("Should never get here!"); } mCursor->mContinueQuery = queryStart + keyRangeClause + directionClause + openLimit; mCursor->mContinueToQuery = queryStart + continueToKeyRangeClause + directionClause + openLimit; return NS_OK; } nsresult Cursor:: OpenOp::DoIndexDatabaseWork(DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(mCursor); MOZ_ASSERT(mCursor->mType == OpenCursorParams::TIndexOpenCursorParams); MOZ_ASSERT(mCursor->mObjectStoreId); MOZ_ASSERT(mCursor->mIndexId); PROFILER_LABEL("IndexedDB", "Cursor::OpenOp::DoIndexDatabaseWork", js::ProfileEntry::Category::STORAGE); const bool usingKeyRange = mOptionalKeyRange.type() == OptionalKeyRange::TSerializedKeyRange; nsCString indexTable = mCursor->mUniqueIndex ? NS_LITERAL_CSTRING("unique_index_data") : NS_LITERAL_CSTRING("index_data"); NS_NAMED_LITERAL_CSTRING(sortColumn, "sort_column"); NS_NAMED_LITERAL_CSTRING(id, "id"); NS_NAMED_LITERAL_CSTRING(openLimit, " LIMIT "); nsAutoCString sortColumnAlias; if (mCursor->IsLocaleAware()) { sortColumnAlias = "SELECT index_table.value, " "index_table.value_locale as sort_column, "; } else { sortColumnAlias = "SELECT index_table.value as sort_column, " "index_table.value_locale, "; } nsAutoCString queryStart = sortColumnAlias + NS_LITERAL_CSTRING( "index_table.object_data_key, " "object_data.file_ids, " "object_data.data " "FROM ") + indexTable + NS_LITERAL_CSTRING(" 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 = :") + id; nsAutoCString keyRangeClause; if (usingKeyRange) { GetBindingClauseForKeyRange(mOptionalKeyRange.get_SerializedKeyRange(), sortColumn, keyRangeClause); } nsAutoCString directionClause = NS_LITERAL_CSTRING(" ORDER BY ") + sortColumn; switch (mCursor->mDirection) { case IDBCursor::NEXT: case IDBCursor::NEXT_UNIQUE: directionClause.AppendLiteral(" ASC, index_table.object_data_key ASC"); break; case IDBCursor::PREV: directionClause.AppendLiteral(" DESC, index_table.object_data_key DESC"); break; case IDBCursor::PREV_UNIQUE: 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. nsCString firstQuery = queryStart + keyRangeClause + directionClause + openLimit + NS_LITERAL_CSTRING("1"); DatabaseConnection::CachedStatement stmt; nsresult rv = aConnection->GetCachedStatement(firstQuery, &stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->BindInt64ByName(id, mCursor->mIndexId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (usingKeyRange) { if (mCursor->IsLocaleAware()) { rv = BindKeyRangeToStatement(mOptionalKeyRange.get_SerializedKeyRange(), stmt, mCursor->mLocale); } else { rv = BindKeyRangeToStatement(mOptionalKeyRange.get_SerializedKeyRange(), stmt); } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } bool hasResult; rv = stmt->ExecuteStep(&hasResult); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (!hasResult) { mResponse = void_t(); return NS_OK; } rv = PopulateResponseFromStatement(stmt, true); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Now we need to make the query to get the next match. NS_NAMED_LITERAL_CSTRING(rangeKey, "range_key"); switch (mCursor->mDirection) { case IDBCursor::NEXT: { Key upper; bool open; GetRangeKeyInfo(false, &upper, &open); if (usingKeyRange && !upper.IsUnset()) { AppendConditionClause(sortColumn, rangeKey, true, !open, queryStart); mCursor->mRangeKey = upper; } mCursor->mContinueQuery = queryStart + NS_LITERAL_CSTRING(" AND sort_column >= :current_key " "AND ( sort_column > :current_key OR " "index_table.object_data_key > :object_key ) " ) + directionClause + openLimit; mCursor->mContinueToQuery = queryStart + NS_LITERAL_CSTRING(" AND sort_column >= :current_key") + directionClause + openLimit; mCursor->mContinuePrimaryKeyQuery = queryStart + NS_LITERAL_CSTRING(" AND sort_column >= :current_key " "AND index_table.object_data_key >= :object_key " ) + directionClause + openLimit; break; } case IDBCursor::NEXT_UNIQUE: { Key upper; bool open; GetRangeKeyInfo(false, &upper, &open); if (usingKeyRange && !upper.IsUnset()) { AppendConditionClause(sortColumn, rangeKey, true, !open, queryStart); mCursor->mRangeKey = upper; } mCursor->mContinueQuery = queryStart + NS_LITERAL_CSTRING(" AND sort_column > :current_key") + directionClause + openLimit; mCursor->mContinueToQuery = queryStart + NS_LITERAL_CSTRING(" AND sort_column >= :current_key") + directionClause + openLimit; break; } case IDBCursor::PREV: { Key lower; bool open; GetRangeKeyInfo(true, &lower, &open); if (usingKeyRange && !lower.IsUnset()) { AppendConditionClause(sortColumn, rangeKey, false, !open, queryStart); mCursor->mRangeKey = lower; } mCursor->mContinueQuery = queryStart + NS_LITERAL_CSTRING(" AND sort_column <= :current_key " "AND ( sort_column < :current_key OR " "index_table.object_data_key < :object_key ) " ) + directionClause + openLimit; mCursor->mContinueToQuery = queryStart + NS_LITERAL_CSTRING(" AND sort_column <= :current_key") + directionClause + openLimit; mCursor->mContinuePrimaryKeyQuery = queryStart + NS_LITERAL_CSTRING(" AND sort_column <= :current_key " "AND index_table.object_data_key <= :object_key " ) + directionClause + openLimit; break; } case IDBCursor::PREV_UNIQUE: { Key lower; bool open; GetRangeKeyInfo(true, &lower, &open); if (usingKeyRange && !lower.IsUnset()) { AppendConditionClause(sortColumn, rangeKey, false, !open, queryStart); mCursor->mRangeKey = lower; } mCursor->mContinueQuery = queryStart + NS_LITERAL_CSTRING(" AND sort_column < :current_key") + directionClause + openLimit; mCursor->mContinueToQuery = queryStart + NS_LITERAL_CSTRING(" AND sort_column <= :current_key") + directionClause + openLimit; break; } default: MOZ_CRASH("Should never get here!"); } return NS_OK; } nsresult Cursor:: OpenOp::DoIndexKeyDatabaseWork(DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(mCursor); MOZ_ASSERT(mCursor->mType == OpenCursorParams::TIndexOpenKeyCursorParams); MOZ_ASSERT(mCursor->mObjectStoreId); MOZ_ASSERT(mCursor->mIndexId); PROFILER_LABEL("IndexedDB", "Cursor::OpenOp::DoIndexKeyDatabaseWork", js::ProfileEntry::Category::STORAGE); const bool usingKeyRange = mOptionalKeyRange.type() == OptionalKeyRange::TSerializedKeyRange; nsCString table = mCursor->mUniqueIndex ? NS_LITERAL_CSTRING("unique_index_data") : NS_LITERAL_CSTRING("index_data"); NS_NAMED_LITERAL_CSTRING(sortColumn, "sort_column"); NS_NAMED_LITERAL_CSTRING(id, "id"); NS_NAMED_LITERAL_CSTRING(openLimit, " LIMIT "); nsAutoCString sortColumnAlias; if (mCursor->IsLocaleAware()) { sortColumnAlias = "SELECT value, " "value_locale as sort_column, "; } else { sortColumnAlias = "SELECT value as sort_column, " "value_locale, "; } nsAutoCString queryStart = sortColumnAlias + NS_LITERAL_CSTRING( "object_data_key " " FROM ") + table + NS_LITERAL_CSTRING(" WHERE index_id = :") + id; nsAutoCString keyRangeClause; if (usingKeyRange) { GetBindingClauseForKeyRange(mOptionalKeyRange.get_SerializedKeyRange(), sortColumn, keyRangeClause); } nsAutoCString directionClause = NS_LITERAL_CSTRING(" ORDER BY ") + sortColumn; switch (mCursor->mDirection) { case IDBCursor::NEXT: case IDBCursor::NEXT_UNIQUE: directionClause.AppendLiteral(" ASC, object_data_key ASC"); break; case IDBCursor::PREV: directionClause.AppendLiteral(" DESC, object_data_key DESC"); break; case IDBCursor::PREV_UNIQUE: 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. nsCString firstQuery = queryStart + keyRangeClause + directionClause + openLimit + NS_LITERAL_CSTRING("1"); DatabaseConnection::CachedStatement stmt; nsresult rv = aConnection->GetCachedStatement(firstQuery, &stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = stmt->BindInt64ByName(id, mCursor->mIndexId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (usingKeyRange) { if (mCursor->IsLocaleAware()) { rv = BindKeyRangeToStatement(mOptionalKeyRange.get_SerializedKeyRange(), stmt, mCursor->mLocale); } else { rv = BindKeyRangeToStatement(mOptionalKeyRange.get_SerializedKeyRange(), stmt); } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } bool hasResult; rv = stmt->ExecuteStep(&hasResult); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (!hasResult) { mResponse = void_t(); return NS_OK; } rv = PopulateResponseFromStatement(stmt, true); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Now we need to make the query to get the next match. NS_NAMED_LITERAL_CSTRING(rangeKey, "range_key"); switch (mCursor->mDirection) { case IDBCursor::NEXT: { Key upper; bool open; GetRangeKeyInfo(false, &upper, &open); if (usingKeyRange && !upper.IsUnset()) { AppendConditionClause(sortColumn, rangeKey, true, !open, queryStart); mCursor->mRangeKey = upper; } mCursor->mContinueQuery = queryStart + NS_LITERAL_CSTRING(" AND sort_column >= :current_key " "AND ( sort_column > :current_key OR " "object_data_key > :object_key )") + directionClause + openLimit; mCursor->mContinueToQuery = queryStart + NS_LITERAL_CSTRING(" AND sort_column >= :current_key ") + directionClause + openLimit; mCursor->mContinuePrimaryKeyQuery = queryStart + NS_LITERAL_CSTRING(" AND sort_column >= :current_key " "AND object_data_key >= :object_key " ) + directionClause + openLimit; break; } case IDBCursor::NEXT_UNIQUE: { Key upper; bool open; GetRangeKeyInfo(false, &upper, &open); if (usingKeyRange && !upper.IsUnset()) { AppendConditionClause(sortColumn, rangeKey, true, !open, queryStart); mCursor->mRangeKey = upper; } mCursor->mContinueQuery = queryStart + NS_LITERAL_CSTRING(" AND sort_column > :current_key") + directionClause + openLimit; mCursor->mContinueToQuery = queryStart + NS_LITERAL_CSTRING(" AND sort_column >= :current_key") + directionClause + openLimit; break; } case IDBCursor::PREV: { Key lower; bool open; GetRangeKeyInfo(true, &lower, &open); if (usingKeyRange && !lower.IsUnset()) { AppendConditionClause(sortColumn, rangeKey, false, !open, queryStart); mCursor->mRangeKey = lower; } mCursor->mContinueQuery = queryStart + NS_LITERAL_CSTRING(" AND sort_column <= :current_key " "AND ( sort_column < :current_key OR " "object_data_key < :object_key )") + directionClause + openLimit; mCursor->mContinueToQuery = queryStart + NS_LITERAL_CSTRING(" AND sort_column <= :current_key ") + directionClause + openLimit; mCursor->mContinuePrimaryKeyQuery = queryStart + NS_LITERAL_CSTRING(" AND sort_column <= :current_key " "AND object_data_key <= :object_key " ) + directionClause + openLimit; break; } case IDBCursor::PREV_UNIQUE: { Key lower; bool open; GetRangeKeyInfo(true, &lower, &open); if (usingKeyRange && !lower.IsUnset()) { AppendConditionClause(sortColumn, rangeKey, false, !open, queryStart); mCursor->mRangeKey = lower; } mCursor->mContinueQuery = queryStart + NS_LITERAL_CSTRING(" AND sort_column < :current_key") + directionClause + openLimit; mCursor->mContinueToQuery = queryStart + NS_LITERAL_CSTRING(" AND sort_column <= :current_key") + directionClause + openLimit; break; } default: MOZ_CRASH("Should never get here!"); } return NS_OK; } nsresult Cursor:: OpenOp::DoDatabaseWork(DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(mCursor); MOZ_ASSERT(mCursor->mContinueQuery.IsEmpty()); MOZ_ASSERT(mCursor->mContinueToQuery.IsEmpty()); MOZ_ASSERT(mCursor->mContinuePrimaryKeyQuery.IsEmpty()); MOZ_ASSERT(mCursor->mKey.IsUnset()); MOZ_ASSERT(mCursor->mRangeKey.IsUnset()); PROFILER_LABEL("IndexedDB", "Cursor::OpenOp::DoDatabaseWork", js::ProfileEntry::Category::STORAGE); nsresult rv; switch (mCursor->mType) { case OpenCursorParams::TObjectStoreOpenCursorParams: rv = DoObjectStoreDatabaseWork(aConnection); break; case OpenCursorParams::TObjectStoreOpenKeyCursorParams: rv = DoObjectStoreKeyDatabaseWork(aConnection); break; case OpenCursorParams::TIndexOpenCursorParams: rv = DoIndexDatabaseWork(aConnection); break; case OpenCursorParams::TIndexOpenKeyCursorParams: rv = DoIndexKeyDatabaseWork(aConnection); break; default: MOZ_CRASH("Should never get here!"); } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } nsresult Cursor:: OpenOp::SendSuccessResult() { AssertIsOnOwningThread(); MOZ_ASSERT(mCursor); MOZ_ASSERT(mCursor->mCurrentlyRunningOp == this); MOZ_ASSERT(mResponse.type() != CursorResponse::T__None); MOZ_ASSERT_IF(mResponse.type() == CursorResponse::Tvoid_t, mCursor->mKey.IsUnset()); MOZ_ASSERT_IF(mResponse.type() == CursorResponse::Tvoid_t, mCursor->mSortKey.IsUnset()); MOZ_ASSERT_IF(mResponse.type() == CursorResponse::Tvoid_t, mCursor->mRangeKey.IsUnset()); MOZ_ASSERT_IF(mResponse.type() == CursorResponse::Tvoid_t, mCursor->mObjectKey.IsUnset()); if (IsActorDestroyed()) { return NS_ERROR_DOM_INDEXEDDB_ABORT_ERR; } mCursor->SendResponseInternal(mResponse, mFiles); #ifdef DEBUG mResponseSent = true; #endif return NS_OK; } nsresult Cursor:: ContinueOp::DoDatabaseWork(DatabaseConnection* aConnection) { MOZ_ASSERT(aConnection); aConnection->AssertIsOnConnectionThread(); MOZ_ASSERT(mCursor); MOZ_ASSERT(mCursor->mObjectStoreId); MOZ_ASSERT(!mCursor->mContinueQuery.IsEmpty()); MOZ_ASSERT(!mCursor->mContinueToQuery.IsEmpty()); MOZ_ASSERT(!mCursor->mKey.IsUnset()); const bool isIndex = mCursor->mType == OpenCursorParams::TIndexOpenCursorParams || mCursor->mType == OpenCursorParams::TIndexOpenKeyCursorParams; MOZ_ASSERT_IF(isIndex && (mCursor->mDirection == IDBCursor::NEXT || mCursor->mDirection == IDBCursor::PREV), !mCursor->mContinuePrimaryKeyQuery.IsEmpty()); MOZ_ASSERT_IF(isIndex, mCursor->mIndexId); MOZ_ASSERT_IF(isIndex, !mCursor->mObjectKey.IsUnset()); PROFILER_LABEL("IndexedDB", "Cursor::ContinueOp::DoDatabaseWork", js::ProfileEntry::Category::STORAGE); // 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 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. // Note: Changing the number or order of SELECT columns in the query will // require changes to CursorOpBase::PopulateResponseFromStatement. bool hasContinueKey = false; bool hasContinuePrimaryKey = false; uint32_t advanceCount = 1; Key& currentKey = mCursor->IsLocaleAware() ? mCursor->mSortKey : mCursor->mKey; switch (mParams.type()) { case CursorRequestParams::TContinueParams: if (!mParams.get_ContinueParams().key().IsUnset()) { hasContinueKey = true; currentKey = 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 == IDBCursor::NEXT || mCursor->mDirection == IDBCursor::PREV); hasContinueKey = true; hasContinuePrimaryKey = true; currentKey = mParams.get_ContinuePrimaryKeyParams().key(); break; case CursorRequestParams::TAdvanceParams: advanceCount = mParams.get_AdvanceParams().count(); break; default: MOZ_CRASH("Should never get here!"); } const nsCString& continueQuery = hasContinuePrimaryKey ? mCursor->mContinuePrimaryKeyQuery : hasContinueKey ? mCursor->mContinueToQuery : mCursor->mContinueQuery; MOZ_ASSERT(advanceCount > 0); nsAutoCString countString; countString.AppendInt(advanceCount); nsCString query = continueQuery + countString; NS_NAMED_LITERAL_CSTRING(currentKeyName, "current_key"); NS_NAMED_LITERAL_CSTRING(rangeKeyName, "range_key"); NS_NAMED_LITERAL_CSTRING(objectKeyName, "object_key"); const bool usingRangeKey = !mCursor->mRangeKey.IsUnset(); DatabaseConnection::CachedStatement stmt; nsresult rv = aConnection->GetCachedStatement(query, &stmt); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } int64_t id = isIndex ? mCursor->mIndexId : mCursor->mObjectStoreId; rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("id"), id); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Bind current key. rv = currentKey.BindToStatement(stmt, currentKeyName); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Bind range key if it is specified. if (usingRangeKey) { rv = mCursor->mRangeKey.BindToStatement(stmt, rangeKeyName); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } // Bind object key if duplicates are allowed and we're not continuing to a // specific key. if (isIndex && !hasContinueKey && (mCursor->mDirection == IDBCursor::NEXT || mCursor->mDirection == IDBCursor::PREV)) { rv = mCursor->mObjectKey.BindToStatement(stmt, objectKeyName); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } // Bind object key if primaryKey is specified. if (hasContinuePrimaryKey) { rv = mParams.get_ContinuePrimaryKeyParams().primaryKey() .BindToStatement(stmt, objectKeyName); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } bool hasResult; for (uint32_t index = 0; index < advanceCount; index++) { rv = stmt->ExecuteStep(&hasResult); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (!hasResult) { mCursor->mKey.Unset(); mCursor->mSortKey.Unset(); mCursor->mRangeKey.Unset(); mCursor->mObjectKey.Unset(); mResponse = void_t(); return NS_OK; } } rv = PopulateResponseFromStatement(stmt, true); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } nsresult Cursor:: ContinueOp::SendSuccessResult() { AssertIsOnOwningThread(); MOZ_ASSERT(mCursor); MOZ_ASSERT(mCursor->mCurrentlyRunningOp == this); MOZ_ASSERT_IF(mResponse.type() == CursorResponse::Tvoid_t, mCursor->mKey.IsUnset()); MOZ_ASSERT_IF(mResponse.type() == CursorResponse::Tvoid_t, mCursor->mRangeKey.IsUnset()); MOZ_ASSERT_IF(mResponse.type() == CursorResponse::Tvoid_t, mCursor->mObjectKey.IsUnset()); if (IsActorDestroyed()) { return NS_ERROR_DOM_INDEXEDDB_ABORT_ERR; } mCursor->SendResponseInternal(mResponse, mFiles); #ifdef DEBUG mResponseSent = true; #endif 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 } bool Utils::RecvDeleteMe() { AssertIsOnBackgroundThread(); MOZ_ASSERT(!mActorDestroyed); return PBackgroundIndexedDBUtilsParent::Send__delete__(this); } bool Utils::RecvGetFileReferences(const PersistenceType& aPersistenceType, const nsCString& aOrigin, const nsString& aDatabaseName, const int64_t& aFileId, int32_t* aRefCnt, int32_t* aDBRefCnt, int32_t* aSliceRefCnt, bool* aResult) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aRefCnt); MOZ_ASSERT(aDBRefCnt); MOZ_ASSERT(aSliceRefCnt); MOZ_ASSERT(aResult); MOZ_ASSERT(!mActorDestroyed); if (NS_WARN_IF(!IndexedDatabaseManager::Get() || !QuotaManager::Get())) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(!IndexedDatabaseManager::InTestingMode())) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(aPersistenceType != quota::PERSISTENCE_TYPE_PERSISTENT && aPersistenceType != quota::PERSISTENCE_TYPE_TEMPORARY && aPersistenceType != quota::PERSISTENCE_TYPE_DEFAULT)) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(aOrigin.IsEmpty())) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(aDatabaseName.IsEmpty())) { ASSERT_UNLESS_FUZZING(); return false; } if (NS_WARN_IF(aFileId == 0)) { ASSERT_UNLESS_FUZZING(); return false; } RefPtr<GetFileReferencesHelper> helper = new GetFileReferencesHelper(aPersistenceType, aOrigin, aDatabaseName, aFileId); nsresult rv = helper->DispatchAndReturnFileReferences(aRefCnt, aDBRefCnt, aSliceRefCnt, aResult); if (NS_WARN_IF(NS_FAILED(rv))) { return false; } return true; } nsresult GetFileReferencesHelper::DispatchAndReturnFileReferences(int32_t* aMemRefCnt, int32_t* aDBRefCnt, int32_t* aSliceRefCnt, bool* aResult) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aMemRefCnt); MOZ_ASSERT(aDBRefCnt); MOZ_ASSERT(aSliceRefCnt); MOZ_ASSERT(aResult); QuotaManager* quotaManager = QuotaManager::Get(); MOZ_ASSERT(quotaManager); nsresult rv = quotaManager->IOThread()->Dispatch(this, NS_DISPATCH_NORMAL); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } mozilla::MutexAutoLock autolock(mMutex); while (mWaiting) { mCondVar.Wait(); } *aMemRefCnt = mMemRefCnt; *aDBRefCnt = mDBRefCnt; *aSliceRefCnt = mSliceRefCnt; *aResult = mResult; return NS_OK; } NS_IMETHODIMP GetFileReferencesHelper::Run() { AssertIsOnIOThread(); IndexedDatabaseManager* mgr = IndexedDatabaseManager::Get(); MOZ_ASSERT(mgr); RefPtr<FileManager> fileManager = mgr->GetFileManager(mPersistenceType, mOrigin, mDatabaseName); if (fileManager) { RefPtr<FileInfo> fileInfo = fileManager->GetFileInfo(mFileId); if (fileInfo) { fileInfo->GetReferences(&mMemRefCnt, &mDBRefCnt, &mSliceRefCnt); if (mMemRefCnt != -1) { // We added an extra temp ref, so account for that accordingly. mMemRefCnt--; } mResult = true; } } mozilla::MutexAutoLock lock(mMutex); MOZ_ASSERT(mWaiting); mWaiting = false; mCondVar.Notify(); return NS_OK; } NS_IMETHODIMP FlushPendingFileDeletionsRunnable::Run() { MOZ_ASSERT(NS_IsMainThread()); RefPtr<IndexedDatabaseManager> mgr = IndexedDatabaseManager::Get(); if (NS_WARN_IF(!mgr)) { return NS_ERROR_FAILURE; } nsresult rv = mgr->FlushPendingFileDeletions(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } void PermissionRequestHelper::OnPromptComplete(PermissionValue aPermissionValue) { MOZ_ASSERT(NS_IsMainThread()); if (!mActorDestroyed) { Unused << PIndexedDBPermissionRequestParent::Send__delete__(this, aPermissionValue); } } void PermissionRequestHelper::ActorDestroy(ActorDestroyReason aWhy) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(!mActorDestroyed); mActorDestroyed = true; } #ifdef DEBUG NS_IMPL_ISUPPORTS(DEBUGThreadSlower, nsIThreadObserver) NS_IMETHODIMP DEBUGThreadSlower::OnDispatchedEvent(nsIThreadInternal* /* aThread */) { 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()); MOZ_ASSERT(mFileManager); nsCOMPtr<nsIFile> fileDirectory = mFileManager->GetCheckedDirectory(); if (NS_WARN_IF(!fileDirectory)) { return NS_ERROR_FAILURE; } nsCOMPtr<nsIFile> journalDirectory = mFileManager->EnsureJournalDirectory(); if (NS_WARN_IF(!journalDirectory)) { return NS_ERROR_FAILURE; } DebugOnly<bool> exists; MOZ_ASSERT(NS_SUCCEEDED(journalDirectory->Exists(&exists))); MOZ_ASSERT(exists); DebugOnly<bool> isDirectory; MOZ_ASSERT(NS_SUCCEEDED(journalDirectory->IsDirectory(&isDirectory))); MOZ_ASSERT(isDirectory); mFileDirectory = Move(fileDirectory); mJournalDirectory= Move(journalDirectory); return NS_OK; } already_AddRefed<nsIFile> FileHelper::GetFile(FileInfo* aFileInfo) { MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(aFileInfo); MOZ_ASSERT(mFileManager); MOZ_ASSERT(mFileDirectory); const int64_t fileId = aFileInfo->Id(); MOZ_ASSERT(fileId > 0); nsCOMPtr<nsIFile> file = mFileManager->GetFileForId(mFileDirectory, fileId); return file.forget(); } already_AddRefed<nsIFile> FileHelper::GetCheckedFile(FileInfo* aFileInfo) { MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(aFileInfo); MOZ_ASSERT(mFileManager); MOZ_ASSERT(mFileDirectory); const int64_t fileId = aFileInfo->Id(); MOZ_ASSERT(fileId > 0); nsCOMPtr<nsIFile> file = mFileManager->GetCheckedFileForId(mFileDirectory, fileId); return file.forget(); } already_AddRefed<nsIFile> FileHelper::GetJournalFile(FileInfo* aFileInfo) { MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(aFileInfo); MOZ_ASSERT(mFileManager); MOZ_ASSERT(mJournalDirectory); const int64_t fileId = aFileInfo->Id(); MOZ_ASSERT(fileId > 0); nsCOMPtr<nsIFile> file = mFileManager->GetFileForId(mJournalDirectory, fileId); return file.forget(); } nsresult FileHelper::CreateFileFromStream(nsIFile* aFile, nsIFile* aJournalFile, nsIInputStream* aInputStream, bool aCompress) { MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(aFile); MOZ_ASSERT(aJournalFile); MOZ_ASSERT(aInputStream); MOZ_ASSERT(mFileManager); MOZ_ASSERT(mFileDirectory); MOZ_ASSERT(mJournalDirectory); bool exists; nsresult rv = aFile->Exists(&exists); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // 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) { bool isFile; rv = aFile->IsFile(&isFile); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (NS_WARN_IF(!isFile)) { return NS_ERROR_FAILURE; } rv = aJournalFile->Exists(&exists); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (NS_WARN_IF(!exists)) { return NS_ERROR_FAILURE; } rv = aJournalFile->IsFile(&isFile); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (NS_WARN_IF(!isFile)) { return NS_ERROR_FAILURE; } IDB_WARNING("Deleting orphaned file!"); rv = RemoveFile(aFile, aJournalFile); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } // Create a journal file first. rv = aJournalFile->Create(nsIFile::NORMAL_FILE_TYPE, 0644); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Now try to copy the stream. RefPtr<FileOutputStream> fileOutputStream = FileOutputStream::Create(mFileManager->Type(), mFileManager->Group(), mFileManager->Origin(), aFile); if (NS_WARN_IF(!fileOutputStream)) { return NS_ERROR_FAILURE; } if (aCompress) { RefPtr<SnappyCompressOutputStream> snappyOutputStream = new SnappyCompressOutputStream(fileOutputStream); UniquePtr<char[]> buffer(new char[snappyOutputStream->BlockSize()]); rv = SyncCopy(aInputStream, snappyOutputStream, buffer.get(), snappyOutputStream->BlockSize()); } else { char buffer[kFileCopyBufferSize]; rv = SyncCopy(aInputStream, fileOutputStream, buffer, kFileCopyBufferSize); } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } nsresult FileHelper::ReplaceFile(nsIFile* aFile, nsIFile* aNewFile, nsIFile* aNewJournalFile) { MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(aFile); MOZ_ASSERT(aNewFile); MOZ_ASSERT(aNewJournalFile); MOZ_ASSERT(mFileManager); MOZ_ASSERT(mFileDirectory); MOZ_ASSERT(mJournalDirectory); nsresult rv; int64_t fileSize; if (mFileManager->EnforcingQuota()) { rv = aFile->GetFileSize(&fileSize); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } nsAutoString fileName; rv = aFile->GetLeafName(fileName); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = aNewFile->RenameTo(nullptr, fileName); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (mFileManager->EnforcingQuota()) { QuotaManager* quotaManager = QuotaManager::Get(); MOZ_ASSERT(quotaManager); quotaManager->DecreaseUsageForOrigin(mFileManager->Type(), mFileManager->Group(), mFileManager->Origin(), fileSize); } rv = aNewJournalFile->Remove(false); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } nsresult FileHelper::RemoveFile(nsIFile* aFile, nsIFile* aJournalFile) { nsresult rv; int64_t fileSize; if (mFileManager->EnforcingQuota()) { rv = aFile->GetFileSize(&fileSize); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } rv = aFile->Remove(false); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (mFileManager->EnforcingQuota()) { QuotaManager* quotaManager = QuotaManager::Get(); MOZ_ASSERT(quotaManager); quotaManager->DecreaseUsageForOrigin(mFileManager->Type(), mFileManager->Group(), mFileManager->Origin(), fileSize); } rv = aJournalFile->Remove(false); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } already_AddRefed<FileInfo> FileHelper::GetNewFileInfo() { MOZ_ASSERT(mFileManager); return mFileManager->GetNewFileInfo(); } nsresult FileHelper::SyncCopy(nsIInputStream* aInputStream, nsIOutputStream* aOutputStream, char* aBuffer, uint32_t aBufferSize) { MOZ_ASSERT(!IsOnBackgroundThread()); MOZ_ASSERT(aInputStream); MOZ_ASSERT(aOutputStream); PROFILER_LABEL("IndexedDB", "FileHelper::SyncCopy", js::ProfileEntry::Category::STORAGE); nsresult rv; do { uint32_t numRead; rv = aInputStream->Read(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 indexedDB } // namespace dom } // namespace mozilla #undef IDB_MOBILE #undef IDB_DEBUG_LOG #undef ASSERT_UNLESS_FUZZING #undef DISABLE_ASSERTS_FOR_FUZZING