/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "IDBObjectStore.h" #include #include #include "IDBCursorType.h" #include "IDBDatabase.h" #include "IDBEvents.h" #include "IDBFactory.h" #include "IDBIndex.h" #include "IDBKeyRange.h" #include "IDBMutableFile.h" #include "IDBRequest.h" #include "IDBTransaction.h" #include "IndexedDatabase.h" #include "IndexedDatabaseInlines.h" #include "IndexedDatabaseManager.h" #include "IndexedDBCommon.h" #include "KeyPath.h" #include "ProfilerHelpers.h" #include "ReportInternalError.h" #include "js/Array.h" // JS::GetArrayLength, JS::IsArrayObject #include "js/Class.h" #include "js/Date.h" #include "js/Object.h" // JS::GetClass #include "js/PropertyAndElement.h" // JS_GetProperty, JS_GetPropertyById, JS_HasOwnProperty, JS_HasOwnPropertyById #include "js/StructuredClone.h" #include "mozilla/EndianUtils.h" #include "mozilla/ErrorResult.h" #include "mozilla/ResultExtensions.h" #include "mozilla/dom/BindingUtils.h" #include "mozilla/dom/BlobBinding.h" #include "mozilla/dom/File.h" #include "mozilla/dom/IDBMutableFileBinding.h" #include "mozilla/dom/IDBObjectStoreBinding.h" #include "mozilla/dom/MemoryBlobImpl.h" #include "mozilla/dom/StreamBlobImpl.h" #include "mozilla/dom/StructuredCloneHolder.h" #include "mozilla/dom/StructuredCloneTags.h" #include "mozilla/dom/indexedDB/PBackgroundIDBSharedTypes.h" #include "mozilla/ipc/BackgroundChild.h" #include "mozilla/ipc/PBackgroundSharedTypes.h" #include "nsCOMPtr.h" #include "nsStreamUtils.h" #include "nsStringStream.h" // Include this last to avoid path problems on Windows. #include "ActorsChild.h" namespace mozilla::dom { using namespace mozilla::dom::indexedDB; using namespace mozilla::dom::quota; using namespace mozilla::ipc; namespace { Result MakeIndexUpdateInfo( const int64_t aIndexID, const Key& aKey, const nsCString& aLocale) { IndexUpdateInfo indexUpdateInfo; indexUpdateInfo.indexId() = aIndexID; indexUpdateInfo.value() = aKey; if (!aLocale.IsEmpty()) { QM_TRY_UNWRAP(indexUpdateInfo.localizedValue(), aKey.ToLocaleAwareKey(aLocale)); } return indexUpdateInfo; } } // namespace struct IDBObjectStore::StructuredCloneWriteInfo { JSAutoStructuredCloneBuffer mCloneBuffer; nsTArray mFiles; IDBDatabase* mDatabase; uint64_t mOffsetToKeyProp; explicit StructuredCloneWriteInfo(IDBDatabase* aDatabase) : mCloneBuffer(JS::StructuredCloneScope::DifferentProcessForIndexedDB, nullptr, nullptr), mDatabase(aDatabase), mOffsetToKeyProp(0) { MOZ_ASSERT(aDatabase); MOZ_COUNT_CTOR(StructuredCloneWriteInfo); } StructuredCloneWriteInfo(StructuredCloneWriteInfo&& aCloneWriteInfo) noexcept : mCloneBuffer(std::move(aCloneWriteInfo.mCloneBuffer)), mFiles(std::move(aCloneWriteInfo.mFiles)), mDatabase(aCloneWriteInfo.mDatabase), mOffsetToKeyProp(aCloneWriteInfo.mOffsetToKeyProp) { MOZ_ASSERT(mDatabase); MOZ_COUNT_CTOR(StructuredCloneWriteInfo); aCloneWriteInfo.mOffsetToKeyProp = 0; } MOZ_COUNTED_DTOR(StructuredCloneWriteInfo) }; // Used by ValueWrapper::Clone to hold strong references to any blob-like // objects through the clone process. This is necessary because: // - The structured clone process may trigger content code via getters/other // which can potentially cause existing strong references to be dropped, // necessitating the clone to hold its own strong references. // - The structured clone can abort partway through, so it's necessary to track // what strong references have been acquired so that they can be freed even // if a de-serialization does not occur. struct IDBObjectStore::StructuredCloneInfo { nsTArray mFiles; }; namespace { struct MOZ_STACK_CLASS GetAddInfoClosure final { IDBObjectStore::StructuredCloneWriteInfo& mCloneWriteInfo; JS::Handle mValue; GetAddInfoClosure(IDBObjectStore::StructuredCloneWriteInfo& aCloneWriteInfo, JS::Handle aValue) : mCloneWriteInfo(aCloneWriteInfo), mValue(aValue) { MOZ_COUNT_CTOR(GetAddInfoClosure); } MOZ_COUNTED_DTOR(GetAddInfoClosure) }; MovingNotNull> GenerateRequest( JSContext* aCx, IDBObjectStore* aObjectStore) { MOZ_ASSERT(aObjectStore); aObjectStore->AssertIsOnOwningThread(); auto transaction = aObjectStore->AcquireTransaction(); auto* const database = transaction->Database(); return IDBRequest::Create(aCx, aObjectStore, database, std::move(transaction)); } bool StructuredCloneWriteCallback(JSContext* aCx, JSStructuredCloneWriter* aWriter, JS::Handle aObj, bool* aSameProcessRequired, void* aClosure) { MOZ_ASSERT(aCx); MOZ_ASSERT(aWriter); MOZ_ASSERT(aClosure); auto* const cloneWriteInfo = static_cast(aClosure); if (JS::GetClass(aObj) == IDBObjectStore::DummyPropClass()) { MOZ_ASSERT(!cloneWriteInfo->mOffsetToKeyProp); cloneWriteInfo->mOffsetToKeyProp = js::GetSCOffset(aWriter); uint64_t value = 0; // Omit endian swap return JS_WriteBytes(aWriter, &value, sizeof(value)); } // UNWRAP_OBJECT calls might mutate this. JS::Rooted obj(aCx, aObj); IDBMutableFile* mutableFile; if (NS_SUCCEEDED(UNWRAP_OBJECT(IDBMutableFile, &obj, mutableFile))) { if (cloneWriteInfo->mDatabase->IsFileHandleDisabled()) { return false; } IDBDatabase* const database = mutableFile->Database(); MOZ_ASSERT(database); // Throw when trying to store IDBMutableFile objects that live in a // different database. if (database != cloneWriteInfo->mDatabase) { MOZ_ASSERT(!SameCOMIdentity(database, cloneWriteInfo->mDatabase)); if (database->Name() != cloneWriteInfo->mDatabase->Name()) { return false; } nsCString fileOrigin, databaseOrigin; PersistenceType filePersistenceType, databasePersistenceType; if (NS_WARN_IF(NS_FAILED( database->GetQuotaInfo(fileOrigin, &filePersistenceType)))) { return false; } if (NS_WARN_IF(NS_FAILED(cloneWriteInfo->mDatabase->GetQuotaInfo( databaseOrigin, &databasePersistenceType)))) { return false; } if (filePersistenceType != databasePersistenceType || fileOrigin != databaseOrigin) { return false; } } if (cloneWriteInfo->mFiles.Length() > size_t(UINT32_MAX)) { MOZ_ASSERT(false, "Fix the structured clone data to use a bigger type!"); return false; } const uint32_t index = cloneWriteInfo->mFiles.Length(); const NS_ConvertUTF16toUTF8 convType(mutableFile->Type()); const uint32_t convTypeLength = NativeEndian::swapToLittleEndian(convType.Length()); const NS_ConvertUTF16toUTF8 convName(mutableFile->Name()); const uint32_t convNameLength = NativeEndian::swapToLittleEndian(convName.Length()); if (!JS_WriteUint32Pair(aWriter, SCTAG_DOM_MUTABLEFILE, uint32_t(index)) || !JS_WriteBytes(aWriter, &convTypeLength, sizeof(uint32_t)) || !JS_WriteBytes(aWriter, convType.get(), convType.Length()) || !JS_WriteBytes(aWriter, &convNameLength, sizeof(uint32_t)) || !JS_WriteBytes(aWriter, convName.get(), convName.Length())) { return false; } cloneWriteInfo->mFiles.EmplaceBack(mutableFile); return true; } { Blob* blob = nullptr; if (NS_SUCCEEDED(UNWRAP_OBJECT(Blob, &obj, blob))) { ErrorResult rv; const uint64_t nativeEndianSize = blob->GetSize(rv); MOZ_ASSERT(!rv.Failed()); const uint64_t size = NativeEndian::swapToLittleEndian(nativeEndianSize); nsString type; blob->GetType(type); const NS_ConvertUTF16toUTF8 convType(type); const uint32_t convTypeLength = NativeEndian::swapToLittleEndian(convType.Length()); if (cloneWriteInfo->mFiles.Length() > size_t(UINT32_MAX)) { MOZ_ASSERT(false, "Fix the structured clone data to use a bigger type!"); return false; } const uint32_t index = cloneWriteInfo->mFiles.Length(); if (!JS_WriteUint32Pair(aWriter, blob->IsFile() ? SCTAG_DOM_FILE : SCTAG_DOM_BLOB, index) || !JS_WriteBytes(aWriter, &size, sizeof(size)) || !JS_WriteBytes(aWriter, &convTypeLength, sizeof(convTypeLength)) || !JS_WriteBytes(aWriter, convType.get(), convType.Length())) { return false; } const RefPtr file = blob->ToFile(); if (file) { ErrorResult rv; const int64_t nativeEndianLastModifiedDate = file->GetLastModified(rv); MOZ_ALWAYS_TRUE(!rv.Failed()); const int64_t lastModifiedDate = NativeEndian::swapToLittleEndian(nativeEndianLastModifiedDate); nsString name; file->GetName(name); const NS_ConvertUTF16toUTF8 convName(name); const uint32_t convNameLength = NativeEndian::swapToLittleEndian(convName.Length()); if (!JS_WriteBytes(aWriter, &lastModifiedDate, sizeof(lastModifiedDate)) || !JS_WriteBytes(aWriter, &convNameLength, sizeof(convNameLength)) || !JS_WriteBytes(aWriter, convName.get(), convName.Length())) { return false; } } cloneWriteInfo->mFiles.EmplaceBack(StructuredCloneFileBase::eBlob, blob); return true; } } return StructuredCloneHolder::WriteFullySerializableObjects(aCx, aWriter, aObj); } bool CopyingStructuredCloneWriteCallback(JSContext* aCx, JSStructuredCloneWriter* aWriter, JS::Handle aObj, bool* aSameProcessRequired, void* aClosure) { MOZ_ASSERT(aCx); MOZ_ASSERT(aWriter); MOZ_ASSERT(aClosure); auto* const cloneInfo = static_cast(aClosure); // UNWRAP_OBJECT calls might mutate this. JS::Rooted obj(aCx, aObj); { Blob* blob = nullptr; if (NS_SUCCEEDED(UNWRAP_OBJECT(Blob, &obj, blob))) { if (cloneInfo->mFiles.Length() > size_t(UINT32_MAX)) { MOZ_ASSERT(false, "Fix the structured clone data to use a bigger type!"); return false; } const uint32_t index = cloneInfo->mFiles.Length(); if (!JS_WriteUint32Pair(aWriter, blob->IsFile() ? SCTAG_DOM_FILE : SCTAG_DOM_BLOB, index)) { return false; } cloneInfo->mFiles.EmplaceBack(StructuredCloneFileBase::eBlob, blob); return true; } } { IDBMutableFile* mutableFile; if (NS_SUCCEEDED(UNWRAP_OBJECT(IDBMutableFile, &obj, mutableFile))) { if (cloneInfo->mFiles.Length() > size_t(UINT32_MAX)) { MOZ_ASSERT(false, "Fix the structured clone data to use a bigger type!"); return false; } const uint32_t index = cloneInfo->mFiles.Length(); if (!JS_WriteUint32Pair(aWriter, SCTAG_DOM_MUTABLEFILE, index)) { return false; } cloneInfo->mFiles.EmplaceBack(mutableFile); return true; } } return StructuredCloneHolder::WriteFullySerializableObjects(aCx, aWriter, aObj); } nsresult GetAddInfoCallback(JSContext* aCx, void* aClosure) { static const JSStructuredCloneCallbacks kStructuredCloneCallbacks = { nullptr /* read */, StructuredCloneWriteCallback /* write */, nullptr /* reportError */, nullptr /* readTransfer */, nullptr /* writeTransfer */, nullptr /* freeTransfer */, nullptr /* canTransfer */, nullptr /* sabCloned */ }; MOZ_ASSERT(aCx); auto* const data = static_cast(aClosure); MOZ_ASSERT(data); data->mCloneWriteInfo.mOffsetToKeyProp = 0; if (!data->mCloneWriteInfo.mCloneBuffer.write(aCx, data->mValue, &kStructuredCloneCallbacks, &data->mCloneWriteInfo)) { return NS_ERROR_DOM_DATA_CLONE_ERR; } return NS_OK; } using indexedDB::WrapAsJSObject; template JSObject* WrapAsJSObject(JSContext* const aCx, T& aBaseObject) { JS::Rooted result(aCx); const bool res = WrapAsJSObject(aCx, aBaseObject, &result); return res ? static_cast(result) : nullptr; } JSObject* CopyingStructuredCloneReadCallback( JSContext* aCx, JSStructuredCloneReader* aReader, const JS::CloneDataPolicy& aCloneDataPolicy, uint32_t aTag, uint32_t aData, void* aClosure) { MOZ_ASSERT(aTag != SCTAG_DOM_FILE_WITHOUT_LASTMODIFIEDDATE); if (aTag == SCTAG_DOM_BLOB || aTag == SCTAG_DOM_FILE || aTag == SCTAG_DOM_MUTABLEFILE) { auto* const cloneInfo = static_cast(aClosure); if (aData >= cloneInfo->mFiles.Length()) { MOZ_ASSERT(false, "Bad index value!"); return nullptr; } StructuredCloneFileChild& file = cloneInfo->mFiles[aData]; switch (static_cast(aTag)) { case SCTAG_DOM_BLOB: MOZ_ASSERT(file.Type() == StructuredCloneFileBase::eBlob); MOZ_ASSERT(!file.Blob().IsFile()); return WrapAsJSObject(aCx, file.MutableBlob()); case SCTAG_DOM_FILE: { MOZ_ASSERT(file.Type() == StructuredCloneFileBase::eBlob); JS::Rooted result(aCx); { // Create a scope so ~RefPtr fires before returning an unwrapped // JS::Value. const RefPtr blob = file.BlobPtr(); MOZ_ASSERT(blob->IsFile()); const RefPtr file = blob->ToFile(); MOZ_ASSERT(file); if (!WrapAsJSObject(aCx, file, &result)) { return nullptr; } } return result; } case SCTAG_DOM_MUTABLEFILE: MOZ_ASSERT(file.Type() == StructuredCloneFileBase::eMutableFile); return WrapAsJSObject(aCx, file.MutableMutableFile()); default: // This cannot be reached due to the if condition before. break; } } return StructuredCloneHolder::ReadFullySerializableObjects(aCx, aReader, aTag); } } // namespace const JSClass IDBObjectStore::sDummyPropJSClass = { "IDBObjectStore Dummy", 0 /* flags */ }; IDBObjectStore::IDBObjectStore(SafeRefPtr aTransaction, ObjectStoreSpec* aSpec) : mTransaction(std::move(aTransaction)), mCachedKeyPath(JS::UndefinedValue()), mSpec(aSpec), mId(aSpec->metadata().id()), mRooted(false) { MOZ_ASSERT(mTransaction); mTransaction->AssertIsOnOwningThread(); MOZ_ASSERT(aSpec); } IDBObjectStore::~IDBObjectStore() { AssertIsOnOwningThread(); if (mRooted) { mozilla::DropJSObjects(this); } } // static RefPtr IDBObjectStore::Create( SafeRefPtr aTransaction, ObjectStoreSpec& aSpec) { MOZ_ASSERT(aTransaction); aTransaction->AssertIsOnOwningThread(); return new IDBObjectStore(std::move(aTransaction), &aSpec); } // static void IDBObjectStore::AppendIndexUpdateInfo( const int64_t aIndexID, const KeyPath& aKeyPath, const bool aMultiEntry, const nsCString& aLocale, JSContext* const aCx, JS::Handle aVal, nsTArray* const aUpdateInfoArray, ErrorResult* const aRv) { // This precondition holds when `aVal` is the result of a structured clone. js::AutoAssertNoContentJS noContentJS(aCx); if (!aMultiEntry) { Key key; *aRv = aKeyPath.ExtractKey(aCx, aVal, key); // If an index's keyPath doesn't match an object, we ignore that object. if (aRv->ErrorCodeIs(NS_ERROR_DOM_INDEXEDDB_DATA_ERR) || key.IsUnset()) { aRv->SuppressException(); return; } if (aRv->Failed()) { return; } QM_TRY_UNWRAP(auto item, MakeIndexUpdateInfo(aIndexID, key, aLocale), QM_VOID, [aRv](const nsresult tryResult) { aRv->Throw(tryResult); }); aUpdateInfoArray->AppendElement(std::move(item)); return; } JS::Rooted val(aCx); if (NS_FAILED(aKeyPath.ExtractKeyAsJSVal(aCx, aVal, val.address()))) { return; } bool isArray; if (NS_WARN_IF(!JS::IsArrayObject(aCx, val, &isArray))) { IDB_REPORT_INTERNAL_ERR(); aRv->Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); return; } if (isArray) { JS::Rooted array(aCx, &val.toObject()); uint32_t arrayLength; if (NS_WARN_IF(!JS::GetArrayLength(aCx, array, &arrayLength))) { IDB_REPORT_INTERNAL_ERR(); aRv->Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); return; } for (uint32_t arrayIndex = 0; arrayIndex < arrayLength; arrayIndex++) { JS::Rooted indexId(aCx); if (NS_WARN_IF(!JS_IndexToId(aCx, arrayIndex, &indexId))) { IDB_REPORT_INTERNAL_ERR(); aRv->Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); return; } bool hasOwnProperty; if (NS_WARN_IF( !JS_HasOwnPropertyById(aCx, array, indexId, &hasOwnProperty))) { IDB_REPORT_INTERNAL_ERR(); aRv->Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); return; } if (!hasOwnProperty) { continue; } JS::Rooted arrayItem(aCx); if (NS_WARN_IF(!JS_GetPropertyById(aCx, array, indexId, &arrayItem))) { IDB_REPORT_INTERNAL_ERR(); aRv->Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); return; } Key value; auto result = value.SetFromJSVal(aCx, arrayItem); if (result.isErr() || value.IsUnset()) { // Not a value we can do anything with, ignore it. if (result.isErr() && result.inspectErr().Is(SpecialValues::Exception)) { result.unwrapErr().AsException().SuppressException(); } continue; } QM_TRY_UNWRAP(auto item, MakeIndexUpdateInfo(aIndexID, value, aLocale), QM_VOID, [aRv](const nsresult tryResult) { aRv->Throw(tryResult); }); aUpdateInfoArray->AppendElement(std::move(item)); } } else { Key value; auto result = value.SetFromJSVal(aCx, val); if (result.isErr() || value.IsUnset()) { // Not a value we can do anything with, ignore it. if (result.isErr() && result.inspectErr().Is(SpecialValues::Exception)) { result.unwrapErr().AsException().SuppressException(); } return; } QM_TRY_UNWRAP(auto item, MakeIndexUpdateInfo(aIndexID, value, aLocale), QM_VOID, [aRv](const nsresult tryResult) { aRv->Throw(tryResult); }); aUpdateInfoArray->AppendElement(std::move(item)); } } // static void IDBObjectStore::ClearCloneReadInfo( StructuredCloneReadInfoChild& aReadInfo) { // This is kind of tricky, we only want to release stuff on the main thread, // but we can end up being called on other threads if we have already been // cleared on the main thread. if (!aReadInfo.HasFiles()) { return; } aReadInfo.ReleaseFiles(); } // static bool IDBObjectStore::DeserializeValue( JSContext* aCx, StructuredCloneReadInfoChild&& aCloneReadInfo, JS::MutableHandle aValue) { MOZ_ASSERT(aCx); if (!aCloneReadInfo.Data().Size()) { aValue.setUndefined(); return true; } MOZ_ASSERT(!(aCloneReadInfo.Data().Size() % sizeof(uint64_t))); static const JSStructuredCloneCallbacks callbacks = { StructuredCloneReadCallback, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}; // FIXME: Consider to use StructuredCloneHolder here and in other // deserializing methods. return JS_ReadStructuredClone( aCx, aCloneReadInfo.Data(), JS_STRUCTURED_CLONE_VERSION, JS::StructuredCloneScope::DifferentProcessForIndexedDB, aValue, JS::CloneDataPolicy(), &callbacks, &aCloneReadInfo); } #ifdef DEBUG void IDBObjectStore::AssertIsOnOwningThread() const { MOZ_ASSERT(mTransaction); mTransaction->AssertIsOnOwningThread(); } #endif // DEBUG void IDBObjectStore::GetAddInfo(JSContext* aCx, ValueWrapper& aValueWrapper, JS::Handle aKeyVal, StructuredCloneWriteInfo& aCloneWriteInfo, Key& aKey, nsTArray& aUpdateInfoArray, ErrorResult& aRv) { // Return DATA_ERR if a key was passed in and this objectStore uses inline // keys. if (!aKeyVal.isUndefined() && HasValidKeyPath()) { aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR); return; } const bool isAutoIncrement = AutoIncrement(); if (!HasValidKeyPath()) { // Out-of-line keys must be passed in. auto result = aKey.SetFromJSVal(aCx, aKeyVal); if (result.isErr()) { aRv = result.unwrapErr().ExtractErrorResult( InvalidMapsTo); return; } } else if (!isAutoIncrement) { if (!aValueWrapper.Clone(aCx)) { aRv.Throw(NS_ERROR_DOM_DATA_CLONE_ERR); return; } aRv = GetKeyPath().ExtractKey(aCx, aValueWrapper.Value(), aKey); if (aRv.Failed()) { return; } } // Return DATA_ERR if no key was specified this isn't an autoIncrement // objectStore. if (aKey.IsUnset() && !isAutoIncrement) { aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR); return; } // Figure out indexes and the index values to update here. if (mSpec->indexes().Length() && !aValueWrapper.Clone(aCx)) { aRv.Throw(NS_ERROR_DOM_DATA_CLONE_ERR); return; } { const nsTArray& indexes = mSpec->indexes(); const uint32_t idxCount = indexes.Length(); aUpdateInfoArray.SetCapacity(idxCount); // Pretty good estimate for (uint32_t idxIndex = 0; idxIndex < idxCount; idxIndex++) { const IndexMetadata& metadata = indexes[idxIndex]; AppendIndexUpdateInfo(metadata.id(), metadata.keyPath(), metadata.multiEntry(), metadata.locale(), aCx, aValueWrapper.Value(), &aUpdateInfoArray, &aRv); if (NS_WARN_IF(aRv.Failed())) { return; } } } if (isAutoIncrement && HasValidKeyPath()) { if (!aValueWrapper.Clone(aCx)) { aRv.Throw(NS_ERROR_DOM_DATA_CLONE_ERR); return; } GetAddInfoClosure data(aCloneWriteInfo, aValueWrapper.Value()); MOZ_ASSERT(aKey.IsUnset()); aRv = GetKeyPath().ExtractOrCreateKey(aCx, aValueWrapper.Value(), aKey, &GetAddInfoCallback, &data); } else { GetAddInfoClosure data(aCloneWriteInfo, aValueWrapper.Value()); aRv = GetAddInfoCallback(aCx, &data); } } RefPtr IDBObjectStore::AddOrPut(JSContext* aCx, ValueWrapper& aValueWrapper, JS::Handle aKey, bool aOverwrite, bool aFromCursor, ErrorResult& aRv) { AssertIsOnOwningThread(); MOZ_ASSERT(aCx); MOZ_ASSERT_IF(aFromCursor, aOverwrite); if (mTransaction->GetMode() == IDBTransaction::Mode::Cleanup || mDeletedSpec) { aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); return nullptr; } if (!mTransaction->IsActive()) { aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); return nullptr; } if (!mTransaction->IsWriteAllowed()) { aRv.Throw(NS_ERROR_DOM_INDEXEDDB_READ_ONLY_ERR); return nullptr; } Key key; StructuredCloneWriteInfo cloneWriteInfo(mTransaction->Database()); nsTArray updateInfos; // According to spec https://w3c.github.io/IndexedDB/#clone-value, // the transaction must be in inactive state during clone mTransaction->TransitionToInactive(); #ifdef DEBUG const uint32_t previousPendingRequestCount{ mTransaction->GetPendingRequestCount()}; #endif GetAddInfo(aCx, aValueWrapper, aKey, cloneWriteInfo, key, updateInfos, aRv); // Check that new requests were rejected in the Inactive state // and possibly in the Finished state, if the transaction has been aborted, // during the structured cloning. MOZ_ASSERT(mTransaction->GetPendingRequestCount() == previousPendingRequestCount); if (!mTransaction->IsAborted()) { mTransaction->TransitionToActive(); } else if (!aRv.Failed()) { aRv.Throw(NS_ERROR_DOM_INDEXEDDB_ABORT_ERR); return nullptr; // It is mandatory to return right after throw } if (aRv.Failed()) { return nullptr; } // Check the size limit of the serialized message which mainly consists of // a StructuredCloneBuffer, an encoded object key, and the encoded index keys. // kMaxIDBMsgOverhead covers the minor stuff not included in this calculation // because the precise calculation would slow down this AddOrPut operation. static const size_t kMaxIDBMsgOverhead = 1024 * 1024; // 1MB const uint32_t maximalSizeFromPref = IndexedDatabaseManager::MaxSerializedMsgSize(); MOZ_ASSERT(maximalSizeFromPref > kMaxIDBMsgOverhead); const size_t kMaxMessageSize = maximalSizeFromPref - kMaxIDBMsgOverhead; const size_t indexUpdateInfoSize = std::accumulate(updateInfos.cbegin(), updateInfos.cend(), 0u, [](size_t old, const IndexUpdateInfo& updateInfo) { return old + updateInfo.value().GetBuffer().Length() + updateInfo.localizedValue().GetBuffer().Length(); }); const size_t messageSize = cloneWriteInfo.mCloneBuffer.data().Size() + key.GetBuffer().Length() + indexUpdateInfoSize; if (messageSize > kMaxMessageSize) { IDB_REPORT_INTERNAL_ERR(); aRv.ThrowUnknownError( nsPrintfCString("The serialized value is too large" " (size=%zu bytes, max=%zu bytes).", messageSize, kMaxMessageSize)); return nullptr; } ObjectStoreAddPutParams commonParams; commonParams.objectStoreId() = Id(); commonParams.cloneInfo().data().data = std::move(cloneWriteInfo.mCloneBuffer.data()); commonParams.cloneInfo().offsetToKeyProp() = cloneWriteInfo.mOffsetToKeyProp; commonParams.key() = key; commonParams.indexUpdateInfos() = std::move(updateInfos); // Convert any blobs or mutable files into FileAddInfo. QM_TRY_UNWRAP( commonParams.fileAddInfos(), TransformIntoNewArrayAbortOnErr( cloneWriteInfo.mFiles, [&database = *mTransaction->Database()]( auto& file) -> Result { switch (file.Type()) { case StructuredCloneFileBase::eBlob: { MOZ_ASSERT(file.HasBlob()); MOZ_ASSERT(!file.HasMutableFile()); PBackgroundIDBDatabaseFileChild* const fileActor = database.GetOrCreateFileActorForBlob(file.MutableBlob()); if (NS_WARN_IF(!fileActor)) { IDB_REPORT_INTERNAL_ERR(); return Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); } return FileAddInfo{fileActor, StructuredCloneFileBase::eBlob}; } case StructuredCloneFileBase::eMutableFile: { MOZ_ASSERT(file.HasMutableFile()); MOZ_ASSERT(!file.HasBlob()); PBackgroundMutableFileChild* const mutableFileActor = file.MutableFile().GetBackgroundActor(); if (NS_WARN_IF(!mutableFileActor)) { IDB_REPORT_INTERNAL_ERR(); return Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); } return FileAddInfo{mutableFileActor, StructuredCloneFileBase::eMutableFile}; } case StructuredCloneFileBase::eWasmBytecode: case StructuredCloneFileBase::eWasmCompiled: { MOZ_ASSERT(file.HasBlob()); MOZ_ASSERT(!file.HasMutableFile()); PBackgroundIDBDatabaseFileChild* const fileActor = database.GetOrCreateFileActorForBlob(file.MutableBlob()); if (NS_WARN_IF(!fileActor)) { IDB_REPORT_INTERNAL_ERR(); return Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); } return FileAddInfo{fileActor, file.Type()}; } default: MOZ_CRASH("Should never get here!"); } }, fallible), nullptr, [&aRv](const nsresult result) { aRv = result; }); const auto& params = aOverwrite ? RequestParams{ObjectStorePutParams(std::move(commonParams))} : RequestParams{ObjectStoreAddParams(std::move(commonParams))}; auto request = GenerateRequest(aCx, this).unwrap(); if (!aFromCursor) { if (aOverwrite) { IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( "database(%s).transaction(%s).objectStore(%s).put(%s)", "IDBObjectStore.put(%.0s%.0s%.0s%.0s)", mTransaction->LoggingSerialNumber(), request->LoggingSerialNumber(), IDB_LOG_STRINGIFY(mTransaction->Database()), IDB_LOG_STRINGIFY(*mTransaction), IDB_LOG_STRINGIFY(this), IDB_LOG_STRINGIFY(key)); } else { IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( "database(%s).transaction(%s).objectStore(%s).add(%s)", "IDBObjectStore.add(%.0s%.0s%.0s%.0s)", mTransaction->LoggingSerialNumber(), request->LoggingSerialNumber(), IDB_LOG_STRINGIFY(mTransaction->Database()), IDB_LOG_STRINGIFY(*mTransaction), IDB_LOG_STRINGIFY(this), IDB_LOG_STRINGIFY(key)); } } mTransaction->StartRequest(request, params); mTransaction->InvalidateCursorCaches(); return request; } RefPtr IDBObjectStore::GetAllInternal( bool aKeysOnly, JSContext* aCx, JS::Handle aKey, const Optional& aLimit, ErrorResult& aRv) { AssertIsOnOwningThread(); if (mDeletedSpec) { aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); return nullptr; } if (!mTransaction->IsActive()) { aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); return nullptr; } RefPtr keyRange; IDBKeyRange::FromJSVal(aCx, aKey, &keyRange, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } const int64_t id = Id(); Maybe optionalKeyRange; if (keyRange) { SerializedKeyRange serializedKeyRange; keyRange->ToSerialized(serializedKeyRange); optionalKeyRange.emplace(serializedKeyRange); } const uint32_t limit = aLimit.WasPassed() ? aLimit.Value() : 0; RequestParams params; if (aKeysOnly) { params = ObjectStoreGetAllKeysParams(id, optionalKeyRange, limit); } else { params = ObjectStoreGetAllParams(id, optionalKeyRange, limit); } auto request = GenerateRequest(aCx, this).unwrap(); if (aKeysOnly) { IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( "database(%s).transaction(%s).objectStore(%s)." "getAllKeys(%s, %s)", "IDBObjectStore.getAllKeys(%.0s%.0s%.0s%.0s%.0s)", mTransaction->LoggingSerialNumber(), request->LoggingSerialNumber(), IDB_LOG_STRINGIFY(mTransaction->Database()), IDB_LOG_STRINGIFY(*mTransaction), IDB_LOG_STRINGIFY(this), IDB_LOG_STRINGIFY(keyRange), IDB_LOG_STRINGIFY(aLimit)); } else { IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( "database(%s).transaction(%s).objectStore(%s)." "getAll(%s, %s)", "IDBObjectStore.getAll(%.0s%.0s%.0s%.0s%.0s)", mTransaction->LoggingSerialNumber(), request->LoggingSerialNumber(), IDB_LOG_STRINGIFY(mTransaction->Database()), IDB_LOG_STRINGIFY(*mTransaction), IDB_LOG_STRINGIFY(this), IDB_LOG_STRINGIFY(keyRange), IDB_LOG_STRINGIFY(aLimit)); } // TODO: This is necessary to preserve request ordering only. Proper // sequencing of requests should be done in a more sophisticated manner that // doesn't require invalidating cursor caches (Bug 1580499). mTransaction->InvalidateCursorCaches(); mTransaction->StartRequest(request, params); return request; } RefPtr IDBObjectStore::Add(JSContext* aCx, JS::Handle aValue, JS::Handle aKey, ErrorResult& aRv) { AssertIsOnOwningThread(); ValueWrapper valueWrapper(aCx, aValue); return AddOrPut(aCx, valueWrapper, aKey, false, /* aFromCursor */ false, aRv); } RefPtr IDBObjectStore::Put(JSContext* aCx, JS::Handle aValue, JS::Handle aKey, ErrorResult& aRv) { AssertIsOnOwningThread(); ValueWrapper valueWrapper(aCx, aValue); return AddOrPut(aCx, valueWrapper, aKey, true, /* aFromCursor */ false, aRv); } RefPtr IDBObjectStore::Delete(JSContext* aCx, JS::Handle aKey, ErrorResult& aRv) { AssertIsOnOwningThread(); return DeleteInternal(aCx, aKey, /* aFromCursor */ false, aRv); } RefPtr IDBObjectStore::Get(JSContext* aCx, JS::Handle aKey, ErrorResult& aRv) { AssertIsOnOwningThread(); return GetInternal(/* aKeyOnly */ false, aCx, aKey, aRv); } RefPtr IDBObjectStore::GetKey(JSContext* aCx, JS::Handle aKey, ErrorResult& aRv) { AssertIsOnOwningThread(); return GetInternal(/* aKeyOnly */ true, aCx, aKey, aRv); } RefPtr IDBObjectStore::Clear(JSContext* aCx, ErrorResult& aRv) { AssertIsOnOwningThread(); if (mDeletedSpec) { aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); return nullptr; } if (!mTransaction->IsActive()) { aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); return nullptr; } if (!mTransaction->IsWriteAllowed()) { aRv.Throw(NS_ERROR_DOM_INDEXEDDB_READ_ONLY_ERR); return nullptr; } const ObjectStoreClearParams params = {Id()}; auto request = GenerateRequest(aCx, this).unwrap(); IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( "database(%s).transaction(%s).objectStore(%s).clear()", "IDBObjectStore.clear(%.0s%.0s%.0s)", mTransaction->LoggingSerialNumber(), request->LoggingSerialNumber(), IDB_LOG_STRINGIFY(mTransaction->Database()), IDB_LOG_STRINGIFY(*mTransaction), IDB_LOG_STRINGIFY(this)); mTransaction->InvalidateCursorCaches(); mTransaction->StartRequest(request, params); return request; } RefPtr IDBObjectStore::GetAll(JSContext* aCx, JS::Handle aKey, const Optional& aLimit, ErrorResult& aRv) { AssertIsOnOwningThread(); return GetAllInternal(/* aKeysOnly */ false, aCx, aKey, aLimit, aRv); } RefPtr IDBObjectStore::GetAllKeys(JSContext* aCx, JS::Handle aKey, const Optional& aLimit, ErrorResult& aRv) { AssertIsOnOwningThread(); return GetAllInternal(/* aKeysOnly */ true, aCx, aKey, aLimit, aRv); } RefPtr IDBObjectStore::OpenCursor(JSContext* aCx, JS::Handle aRange, IDBCursorDirection aDirection, ErrorResult& aRv) { AssertIsOnOwningThread(); return OpenCursorInternal(/* aKeysOnly */ false, aCx, aRange, aDirection, aRv); } RefPtr IDBObjectStore::OpenCursor(JSContext* aCx, IDBCursorDirection aDirection, ErrorResult& aRv) { AssertIsOnOwningThread(); return OpenCursorInternal(/* aKeysOnly */ false, aCx, JS::UndefinedHandleValue, aDirection, aRv); } RefPtr IDBObjectStore::OpenKeyCursor(JSContext* aCx, JS::Handle aRange, IDBCursorDirection aDirection, ErrorResult& aRv) { AssertIsOnOwningThread(); return OpenCursorInternal(/* aKeysOnly */ true, aCx, aRange, aDirection, aRv); } RefPtr IDBObjectStore::Index(const nsAString& aName, ErrorResult& aRv) { AssertIsOnOwningThread(); if (mTransaction->IsCommittingOrFinished() || mDeletedSpec) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return nullptr; } const nsTArray& indexMetadatas = mSpec->indexes(); const auto endIndexMetadatas = indexMetadatas.cend(); const auto foundMetadata = std::find_if(indexMetadatas.cbegin(), endIndexMetadatas, [&aName](const auto& indexMetadata) { return indexMetadata.name() == aName; }); if (foundMetadata == endIndexMetadatas) { aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_FOUND_ERR); return nullptr; } const IndexMetadata& metadata = *foundMetadata; const auto endIndexes = mIndexes.cend(); const auto foundIndex = std::find_if(mIndexes.cbegin(), endIndexes, [desiredId = metadata.id()](const auto& index) { return index->Id() == desiredId; }); RefPtr index; if (foundIndex == endIndexes) { index = IDBIndex::Create(this, metadata); MOZ_ASSERT(index); mIndexes.AppendElement(index); } else { index = *foundIndex; } return index; } NS_IMPL_CYCLE_COLLECTION_CLASS(IDBObjectStore) NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(IDBObjectStore) NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mCachedKeyPath) NS_IMPL_CYCLE_COLLECTION_TRACE_END NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(IDBObjectStore) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTransaction) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mIndexes) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDeletedIndexes) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(IDBObjectStore) NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER // Don't unlink mTransaction! NS_IMPL_CYCLE_COLLECTION_UNLINK(mIndexes) NS_IMPL_CYCLE_COLLECTION_UNLINK(mDeletedIndexes) tmp->mCachedKeyPath.setUndefined(); if (tmp->mRooted) { mozilla::DropJSObjects(tmp); tmp->mRooted = false; } NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(IDBObjectStore) NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END NS_IMPL_CYCLE_COLLECTING_ADDREF(IDBObjectStore) NS_IMPL_CYCLE_COLLECTING_RELEASE(IDBObjectStore) JSObject* IDBObjectStore::WrapObject(JSContext* aCx, JS::Handle aGivenProto) { return IDBObjectStore_Binding::Wrap(aCx, this, aGivenProto); } nsIGlobalObject* IDBObjectStore::GetParentObject() const { return mTransaction->GetParentObject(); } void IDBObjectStore::GetKeyPath(JSContext* aCx, JS::MutableHandle aResult, ErrorResult& aRv) { if (!mCachedKeyPath.isUndefined()) { aResult.set(mCachedKeyPath); return; } aRv = GetKeyPath().ToJSVal(aCx, mCachedKeyPath); if (NS_WARN_IF(aRv.Failed())) { return; } if (mCachedKeyPath.isGCThing()) { mozilla::HoldJSObjects(this); mRooted = true; } aResult.set(mCachedKeyPath); } RefPtr IDBObjectStore::IndexNames() { AssertIsOnOwningThread(); return CreateSortedDOMStringList( mSpec->indexes(), [](const auto& index) { return index.name(); }); } RefPtr IDBObjectStore::GetInternal(bool aKeyOnly, JSContext* aCx, JS::Handle aKey, ErrorResult& aRv) { AssertIsOnOwningThread(); if (mDeletedSpec) { aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); return nullptr; } if (!mTransaction->IsActive()) { aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); return nullptr; } RefPtr keyRange; IDBKeyRange::FromJSVal(aCx, aKey, &keyRange, aRv); if (aRv.Failed()) { return nullptr; } if (!keyRange) { // Must specify a key or keyRange for get(). aRv.Throw(NS_ERROR_DOM_INDEXEDDB_KEY_ERR); return nullptr; } const int64_t id = Id(); SerializedKeyRange serializedKeyRange; keyRange->ToSerialized(serializedKeyRange); const auto& params = aKeyOnly ? RequestParams{ObjectStoreGetKeyParams(id, serializedKeyRange)} : RequestParams{ObjectStoreGetParams(id, serializedKeyRange)}; auto request = GenerateRequest(aCx, this).unwrap(); IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( "database(%s).transaction(%s).objectStore(%s).get(%s)", "IDBObjectStore.get(%.0s%.0s%.0s%.0s)", mTransaction->LoggingSerialNumber(), request->LoggingSerialNumber(), IDB_LOG_STRINGIFY(mTransaction->Database()), IDB_LOG_STRINGIFY(*mTransaction), IDB_LOG_STRINGIFY(this), IDB_LOG_STRINGIFY(keyRange)); // TODO: This is necessary to preserve request ordering only. Proper // sequencing of requests should be done in a more sophisticated manner that // doesn't require invalidating cursor caches (Bug 1580499). mTransaction->InvalidateCursorCaches(); mTransaction->StartRequest(request, params); return request; } RefPtr IDBObjectStore::DeleteInternal(JSContext* aCx, JS::Handle aKey, bool aFromCursor, ErrorResult& aRv) { AssertIsOnOwningThread(); if (mDeletedSpec) { aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); return nullptr; } if (!mTransaction->IsActive()) { aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); return nullptr; } if (!mTransaction->IsWriteAllowed()) { aRv.Throw(NS_ERROR_DOM_INDEXEDDB_READ_ONLY_ERR); return nullptr; } RefPtr keyRange; IDBKeyRange::FromJSVal(aCx, aKey, &keyRange, aRv); if (NS_WARN_IF((aRv.Failed()))) { return nullptr; } if (!keyRange) { // Must specify a key or keyRange for delete(). aRv.Throw(NS_ERROR_DOM_INDEXEDDB_KEY_ERR); return nullptr; } ObjectStoreDeleteParams params; params.objectStoreId() = Id(); keyRange->ToSerialized(params.keyRange()); auto request = GenerateRequest(aCx, this).unwrap(); if (!aFromCursor) { IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( "database(%s).transaction(%s).objectStore(%s).delete(%s)", "IDBObjectStore.delete(%.0s%.0s%.0s%.0s)", mTransaction->LoggingSerialNumber(), request->LoggingSerialNumber(), IDB_LOG_STRINGIFY(mTransaction->Database()), IDB_LOG_STRINGIFY(*mTransaction), IDB_LOG_STRINGIFY(this), IDB_LOG_STRINGIFY(keyRange)); } mTransaction->StartRequest(request, params); mTransaction->InvalidateCursorCaches(); return request; } RefPtr IDBObjectStore::CreateIndex( const nsAString& aName, const StringOrStringSequence& aKeyPath, const IDBIndexParameters& aOptionalParameters, ErrorResult& aRv) { AssertIsOnOwningThread(); if (mTransaction->GetMode() != IDBTransaction::Mode::VersionChange || mDeletedSpec) { aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); return nullptr; } const auto transaction = IDBTransaction::MaybeCurrent(); if (!transaction || transaction != mTransaction || !transaction->IsActive()) { aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); return nullptr; } const auto& indexes = mSpec->indexes(); const auto end = indexes.cend(); const auto foundIt = std::find_if( indexes.cbegin(), end, [&aName](const auto& index) { return aName == index.name(); }); if (foundIt != end) { aRv.ThrowConstraintError(nsPrintfCString( "Index named '%s' already exists at index '%zu'", NS_ConvertUTF16toUTF8(aName).get(), foundIt.GetIndex())); return nullptr; } const auto checkValid = [](const auto& keyPath) -> Result { if (!keyPath.IsValid()) { return Err(NS_ERROR_DOM_SYNTAX_ERR); } return keyPath; }; QM_INFOONLY_TRY_UNWRAP( const auto maybeKeyPath, ([&aKeyPath, checkValid]() -> Result { if (aKeyPath.IsString()) { QM_TRY_RETURN( KeyPath::Parse(aKeyPath.GetAsString()).andThen(checkValid)); } MOZ_ASSERT(aKeyPath.IsStringSequence()); if (aKeyPath.GetAsStringSequence().IsEmpty()) { return Err(NS_ERROR_DOM_SYNTAX_ERR); } QM_TRY_RETURN( KeyPath::Parse(aKeyPath.GetAsStringSequence()).andThen(checkValid)); })()); if (!maybeKeyPath) { aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR); return nullptr; } const auto& keyPath = maybeKeyPath.ref(); if (aOptionalParameters.mMultiEntry && keyPath.IsArray()) { aRv.Throw(NS_ERROR_DOM_INVALID_ACCESS_ERR); return nullptr; } #ifdef DEBUG { const auto duplicateIndexName = std::any_of( mIndexes.cbegin(), mIndexes.cend(), [&aName](const auto& index) { return index->Name() == aName; }); MOZ_ASSERT(!duplicateIndexName); } #endif const IndexMetadata* const oldMetadataElements = indexes.IsEmpty() ? nullptr : indexes.Elements(); // With this setup we only validate the passed in locale name by the time we // get to encoding Keys. Maybe we should do it here right away and error out. // Valid locale names are always ASCII as per BCP-47. nsCString locale = NS_LossyConvertUTF16toASCII(aOptionalParameters.mLocale); bool autoLocale = locale.EqualsASCII("auto"); if (autoLocale) { locale = IndexedDatabaseManager::GetLocale(); } IndexMetadata* const metadata = mSpec->indexes().EmplaceBack( transaction->NextIndexId(), nsString(aName), keyPath, locale, aOptionalParameters.mUnique, aOptionalParameters.mMultiEntry, autoLocale); if (oldMetadataElements && oldMetadataElements != indexes.Elements()) { MOZ_ASSERT(indexes.Length() > 1); // Array got moved, update the spec pointers for all live indexes. RefreshSpec(/* aMayDelete */ false); } transaction->CreateIndex(this, *metadata); auto index = IDBIndex::Create(this, *metadata); mIndexes.AppendElement(index); // Don't do this in the macro because we always need to increment the serial // number to keep in sync with the parent. const uint64_t requestSerialNumber = IDBRequest::NextSerialNumber(); IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( "database(%s).transaction(%s).objectStore(%s).createIndex(%s)", "IDBObjectStore.createIndex(%.0s%.0s%.0s%.0s)", mTransaction->LoggingSerialNumber(), requestSerialNumber, IDB_LOG_STRINGIFY(mTransaction->Database()), IDB_LOG_STRINGIFY(*mTransaction), IDB_LOG_STRINGIFY(this), IDB_LOG_STRINGIFY(index)); return index; } void IDBObjectStore::DeleteIndex(const nsAString& aName, ErrorResult& aRv) { AssertIsOnOwningThread(); if (mTransaction->GetMode() != IDBTransaction::Mode::VersionChange || mDeletedSpec) { aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); return; } const auto transaction = IDBTransaction::MaybeCurrent(); if (!transaction || transaction != mTransaction || !transaction->IsActive()) { aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); return; } const auto& metadataArray = mSpec->indexes(); const auto endMetadata = metadataArray.cend(); const auto foundMetadataIt = std::find_if( metadataArray.cbegin(), endMetadata, [&aName](const auto& metadata) { return aName == metadata.name(); }); if (foundMetadataIt == endMetadata) { aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_FOUND_ERR); return; } const auto foundId = foundMetadataIt->id(); MOZ_ASSERT(foundId); // Must remove index from mIndexes before altering the metadata array! { const auto end = mIndexes.end(); const auto foundIt = std::find_if( mIndexes.begin(), end, [foundId](const auto& index) { return index->Id() == foundId; }); // TODO: Or should we assert foundIt != end? if (foundIt != end) { auto& index = *foundIt; index->NoteDeletion(); mDeletedIndexes.EmplaceBack(std::move(index)); mIndexes.RemoveElementAt(foundIt.GetIndex()); } } mSpec->indexes().RemoveElementAt(foundMetadataIt.GetIndex()); RefreshSpec(/* aMayDelete */ false); // Don't do this in the macro because we always need to increment the serial // number to keep in sync with the parent. const uint64_t requestSerialNumber = IDBRequest::NextSerialNumber(); IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( "database(%s).transaction(%s).objectStore(%s)." "deleteIndex(\"%s\")", "IDBObjectStore.deleteIndex(%.0s%.0s%.0s%.0s)", mTransaction->LoggingSerialNumber(), requestSerialNumber, IDB_LOG_STRINGIFY(mTransaction->Database()), IDB_LOG_STRINGIFY(*mTransaction), IDB_LOG_STRINGIFY(this), NS_ConvertUTF16toUTF8(aName).get()); transaction->DeleteIndex(this, foundId); } RefPtr IDBObjectStore::Count(JSContext* aCx, JS::Handle aKey, ErrorResult& aRv) { AssertIsOnOwningThread(); if (mDeletedSpec) { aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); return nullptr; } if (!mTransaction->IsActive()) { aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); return nullptr; } RefPtr keyRange; IDBKeyRange::FromJSVal(aCx, aKey, &keyRange, aRv); if (aRv.Failed()) { return nullptr; } ObjectStoreCountParams params; params.objectStoreId() = Id(); if (keyRange) { SerializedKeyRange serializedKeyRange; keyRange->ToSerialized(serializedKeyRange); params.optionalKeyRange().emplace(serializedKeyRange); } auto request = GenerateRequest(aCx, this).unwrap(); IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( "database(%s).transaction(%s).objectStore(%s).count(%s)", "IDBObjectStore.count(%.0s%.0s%.0s%.0s)", mTransaction->LoggingSerialNumber(), request->LoggingSerialNumber(), IDB_LOG_STRINGIFY(mTransaction->Database()), IDB_LOG_STRINGIFY(*mTransaction), IDB_LOG_STRINGIFY(this), IDB_LOG_STRINGIFY(keyRange)); // TODO: This is necessary to preserve request ordering only. Proper // sequencing of requests should be done in a more sophisticated manner that // doesn't require invalidating cursor caches (Bug 1580499). mTransaction->InvalidateCursorCaches(); mTransaction->StartRequest(request, params); return request; } RefPtr IDBObjectStore::OpenCursorInternal( bool aKeysOnly, JSContext* aCx, JS::Handle aRange, IDBCursorDirection aDirection, ErrorResult& aRv) { AssertIsOnOwningThread(); MOZ_ASSERT(aCx); if (mDeletedSpec) { aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); return nullptr; } if (!mTransaction->IsActive()) { aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); return nullptr; } RefPtr keyRange; IDBKeyRange::FromJSVal(aCx, aRange, &keyRange, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } const int64_t objectStoreId = Id(); Maybe optionalKeyRange; if (keyRange) { SerializedKeyRange serializedKeyRange; keyRange->ToSerialized(serializedKeyRange); optionalKeyRange.emplace(std::move(serializedKeyRange)); } const CommonOpenCursorParams commonParams = { objectStoreId, std::move(optionalKeyRange), aDirection}; // TODO: It would be great if the IPDL generator created a constructor // accepting a CommonOpenCursorParams by value or rvalue reference. const auto params = aKeysOnly ? OpenCursorParams{ObjectStoreOpenKeyCursorParams{commonParams}} : OpenCursorParams{ObjectStoreOpenCursorParams{commonParams}}; auto request = GenerateRequest(aCx, this).unwrap(); if (aKeysOnly) { IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( "database(%s).transaction(%s).objectStore(%s)." "openKeyCursor(%s, %s)", "IDBObjectStore.openKeyCursor(%.0s%.0s%.0s%.0s%.0s)", mTransaction->LoggingSerialNumber(), request->LoggingSerialNumber(), IDB_LOG_STRINGIFY(mTransaction->Database()), IDB_LOG_STRINGIFY(*mTransaction), IDB_LOG_STRINGIFY(this), IDB_LOG_STRINGIFY(keyRange), IDB_LOG_STRINGIFY(aDirection)); } else { IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( "database(%s).transaction(%s).objectStore(%s)." "openCursor(%s, %s)", "IDBObjectStore.openCursor(%.0s%.0s%.0s%.0s%.0s)", mTransaction->LoggingSerialNumber(), request->LoggingSerialNumber(), IDB_LOG_STRINGIFY(mTransaction->Database()), IDB_LOG_STRINGIFY(*mTransaction), IDB_LOG_STRINGIFY(this), IDB_LOG_STRINGIFY(keyRange), IDB_LOG_STRINGIFY(aDirection)); } const auto actor = aKeysOnly ? static_cast>( MakeSafeRefPtr< BackgroundCursorChild>( request, this, aDirection)) : MakeSafeRefPtr>( request, this, aDirection); // TODO: This is necessary to preserve request ordering only. Proper // sequencing of requests should be done in a more sophisticated manner that // doesn't require invalidating cursor caches (Bug 1580499). mTransaction->InvalidateCursorCaches(); mTransaction->OpenCursor(*actor, params); return request; } void IDBObjectStore::RefreshSpec(bool aMayDelete) { AssertIsOnOwningThread(); MOZ_ASSERT_IF(mDeletedSpec, mSpec == mDeletedSpec.get()); auto* const foundObjectStoreSpec = mTransaction->Database()->LookupModifiableObjectStoreSpec( [id = Id()](const auto& objSpec) { return objSpec.metadata().id() == id; }); if (foundObjectStoreSpec) { mSpec = foundObjectStoreSpec; for (auto& index : mIndexes) { index->RefreshMetadata(aMayDelete); } for (auto& index : mDeletedIndexes) { index->RefreshMetadata(false); } } MOZ_ASSERT_IF(!aMayDelete && !mDeletedSpec, foundObjectStoreSpec); if (foundObjectStoreSpec) { MOZ_ASSERT(mSpec != mDeletedSpec.get()); mDeletedSpec = nullptr; } else { NoteDeletion(); } } const ObjectStoreSpec& IDBObjectStore::Spec() const { AssertIsOnOwningThread(); MOZ_ASSERT(mSpec); return *mSpec; } void IDBObjectStore::NoteDeletion() { AssertIsOnOwningThread(); MOZ_ASSERT(mSpec); MOZ_ASSERT(Id() == mSpec->metadata().id()); if (mDeletedSpec) { MOZ_ASSERT(mDeletedSpec.get() == mSpec); return; } // Copy the spec here. mDeletedSpec = MakeUnique(*mSpec); mDeletedSpec->indexes().Clear(); mSpec = mDeletedSpec.get(); for (const auto& index : mIndexes) { index->NoteDeletion(); } } const nsString& IDBObjectStore::Name() const { AssertIsOnOwningThread(); MOZ_ASSERT(mSpec); return mSpec->metadata().name(); } void IDBObjectStore::SetName(const nsAString& aName, ErrorResult& aRv) { AssertIsOnOwningThread(); if (mTransaction->GetMode() != IDBTransaction::Mode::VersionChange || mDeletedSpec) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return; } const auto transaction = IDBTransaction::MaybeCurrent(); if (!transaction || transaction != mTransaction || !transaction->IsActive()) { aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); return; } if (aName == mSpec->metadata().name()) { return; } // Cache logging string of this object store before renaming. const LoggingString loggingOldObjectStore(this); const nsresult rv = transaction->Database()->RenameObjectStore(mSpec->metadata().id(), aName); if (NS_FAILED(rv)) { aRv.Throw(rv); return; } // Don't do this in the macro because we always need to increment the serial // number to keep in sync with the parent. const uint64_t requestSerialNumber = IDBRequest::NextSerialNumber(); IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( "database(%s).transaction(%s).objectStore(%s).rename(%s)", "IDBObjectStore.rename(%.0s%.0s%.0s%.0s)", mTransaction->LoggingSerialNumber(), requestSerialNumber, IDB_LOG_STRINGIFY(mTransaction->Database()), IDB_LOG_STRINGIFY(*mTransaction), loggingOldObjectStore.get(), IDB_LOG_STRINGIFY(this)); transaction->RenameObjectStore(mSpec->metadata().id(), aName); } bool IDBObjectStore::AutoIncrement() const { AssertIsOnOwningThread(); MOZ_ASSERT(mSpec); return mSpec->metadata().autoIncrement(); } const indexedDB::KeyPath& IDBObjectStore::GetKeyPath() const { AssertIsOnOwningThread(); MOZ_ASSERT(mSpec); return mSpec->metadata().keyPath(); } bool IDBObjectStore::HasValidKeyPath() const { AssertIsOnOwningThread(); MOZ_ASSERT(mSpec); return GetKeyPath().IsValid(); } bool IDBObjectStore::ValueWrapper::Clone(JSContext* aCx) { if (mCloned) { return true; } static const JSStructuredCloneCallbacks callbacks = { CopyingStructuredCloneReadCallback /* read */, CopyingStructuredCloneWriteCallback /* write */, nullptr /* reportError */, nullptr /* readTransfer */, nullptr /* writeTransfer */, nullptr /* freeTransfer */, nullptr /* canTransfer */, nullptr /* sabCloned */ }; StructuredCloneInfo cloneInfo; JS::Rooted clonedValue(aCx); if (!JS_StructuredClone(aCx, mValue, &clonedValue, &callbacks, &cloneInfo)) { return false; } mValue = clonedValue; mCloned = true; return true; } } // namespace mozilla::dom