mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-11-03 17:58:55 +02:00
Restore IPC message size limit and introduce explicit structured clone size limit in IndexedDB This patch addresses an issue where large arrays of structured clones or keys (< 64KB each) can accumulate in the IPC message, easily reaching the hard 256 MB IPC limit. Key Changes: - Aligned the IPC and IDB-specific IPC message size limits to prevent exceeding the hard IPC limit. - Adjusted the IPC message size calculation in IndexedDB to properly account for shared memory fallback for large structured clones, ensuring that only necessary data is included in the IPC message. Note, the way some preferences are handled in IndexedDatabaseManager would deserve refactoring/cleanup. Differential Revision: https://phabricator.services.mozilla.com/D235643
1972 lines
62 KiB
C++
1972 lines
62 KiB
C++
/* -*- 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 <numeric>
|
|
#include <utility>
|
|
|
|
#include "IDBCursorType.h"
|
|
#include "IDBDatabase.h"
|
|
#include "IDBEvents.h"
|
|
#include "IDBFactory.h"
|
|
#include "IDBIndex.h"
|
|
#include "IDBKeyRange.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/Document.h"
|
|
#include "mozilla/dom/File.h"
|
|
#include "mozilla/dom/FileList.h"
|
|
#include "mozilla/dom/FileListBinding.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 "nsIGlobalObject.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<IndexUpdateInfo, nsresult> 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<StructuredCloneFileChild> 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<StructuredCloneFileChild> mFiles;
|
|
};
|
|
|
|
namespace {
|
|
|
|
struct MOZ_STACK_CLASS GetAddInfoClosure final {
|
|
IDBObjectStore::StructuredCloneWriteInfo& mCloneWriteInfo;
|
|
JS::Handle<JS::Value> mValue;
|
|
|
|
GetAddInfoClosure(IDBObjectStore::StructuredCloneWriteInfo& aCloneWriteInfo,
|
|
JS::Handle<JS::Value> aValue)
|
|
: mCloneWriteInfo(aCloneWriteInfo), mValue(aValue) {
|
|
MOZ_COUNT_CTOR(GetAddInfoClosure);
|
|
}
|
|
|
|
MOZ_COUNTED_DTOR(GetAddInfoClosure)
|
|
};
|
|
|
|
MovingNotNull<RefPtr<IDBRequest>> 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 WriteBlob(JSContext* aCx, JSStructuredCloneWriter* aWriter,
|
|
Blob* const aBlob,
|
|
IDBObjectStore::StructuredCloneWriteInfo* aCloneWriteInfo) {
|
|
MOZ_ASSERT(aCx);
|
|
MOZ_ASSERT(aWriter);
|
|
MOZ_ASSERT(aBlob);
|
|
MOZ_ASSERT(aCloneWriteInfo);
|
|
|
|
ErrorResult rv;
|
|
const uint64_t nativeEndianSize = aBlob->GetSize(rv);
|
|
MOZ_ASSERT(!rv.Failed());
|
|
|
|
const uint64_t size = NativeEndian::swapToLittleEndian(nativeEndianSize);
|
|
|
|
nsString type;
|
|
aBlob->GetType(type);
|
|
|
|
const NS_ConvertUTF16toUTF8 convType(type);
|
|
const uint32_t convTypeLength =
|
|
NativeEndian::swapToLittleEndian(convType.Length());
|
|
|
|
if (aCloneWriteInfo->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 = aCloneWriteInfo->mFiles.Length();
|
|
|
|
if (!JS_WriteUint32Pair(
|
|
aWriter, aBlob->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> file = aBlob->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;
|
|
}
|
|
}
|
|
|
|
aCloneWriteInfo->mFiles.EmplaceBack(StructuredCloneFileBase::eBlob, aBlob);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool StructuredCloneWriteCallback(JSContext* aCx,
|
|
JSStructuredCloneWriter* aWriter,
|
|
JS::Handle<JSObject*> aObj,
|
|
bool* aSameProcessRequired, void* aClosure) {
|
|
MOZ_ASSERT(aCx);
|
|
MOZ_ASSERT(aWriter);
|
|
MOZ_ASSERT(aClosure);
|
|
|
|
auto* const cloneWriteInfo =
|
|
static_cast<IDBObjectStore::StructuredCloneWriteInfo*>(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<JSObject*> obj(aCx, aObj);
|
|
|
|
{
|
|
FileList* fileList = nullptr;
|
|
if (NS_SUCCEEDED(UNWRAP_OBJECT(FileList, &obj, fileList))) {
|
|
const auto fileListStartIndex = cloneWriteInfo->mFiles.Length();
|
|
const uint32_t fileListLength = fileList->Length();
|
|
|
|
if (size_t(fileListStartIndex) > size_t(UINT32_MAX) - fileListLength) {
|
|
MOZ_ASSERT(false,
|
|
"Fix the structured clone data to use a bigger type!");
|
|
return false;
|
|
}
|
|
|
|
if (!JS_WriteUint32Pair(aWriter, SCTAG_DOM_FILELIST, fileListLength)) {
|
|
return false;
|
|
}
|
|
|
|
for (uint32_t i = 0; i < fileListLength; ++i) {
|
|
File* file = fileList->Item(i);
|
|
if (!WriteBlob(aCx, aWriter, file, cloneWriteInfo)) {
|
|
return false; // Everything should fail
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
{
|
|
Blob* blob = nullptr;
|
|
if (NS_SUCCEEDED(UNWRAP_OBJECT(Blob, &obj, blob))) {
|
|
return WriteBlob(aCx, aWriter, blob, cloneWriteInfo);
|
|
}
|
|
}
|
|
|
|
return StructuredCloneHolder::WriteFullySerializableObjects(aCx, aWriter,
|
|
aObj);
|
|
}
|
|
|
|
bool CopyingWriteBlob(JSContext* aCx, JSStructuredCloneWriter* aWriter,
|
|
Blob* const aBlob,
|
|
IDBObjectStore::StructuredCloneInfo* aCloneInfo) {
|
|
MOZ_ASSERT(aCx);
|
|
MOZ_ASSERT(aWriter);
|
|
MOZ_ASSERT(aBlob);
|
|
MOZ_ASSERT(aCloneInfo);
|
|
|
|
if (aCloneInfo->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 = aCloneInfo->mFiles.Length();
|
|
|
|
if (!JS_WriteUint32Pair(
|
|
aWriter, aBlob->IsFile() ? SCTAG_DOM_FILE : SCTAG_DOM_BLOB, index)) {
|
|
return false;
|
|
}
|
|
|
|
aCloneInfo->mFiles.EmplaceBack(StructuredCloneFileBase::eBlob, aBlob);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool CopyingStructuredCloneWriteCallback(JSContext* aCx,
|
|
JSStructuredCloneWriter* aWriter,
|
|
JS::Handle<JSObject*> aObj,
|
|
bool* aSameProcessRequired,
|
|
void* aClosure) {
|
|
MOZ_ASSERT(aCx);
|
|
MOZ_ASSERT(aWriter);
|
|
MOZ_ASSERT(aClosure);
|
|
|
|
auto* const cloneInfo =
|
|
static_cast<IDBObjectStore::StructuredCloneInfo*>(aClosure);
|
|
|
|
// UNWRAP_OBJECT calls might mutate this.
|
|
JS::Rooted<JSObject*> obj(aCx, aObj);
|
|
|
|
{
|
|
FileList* fileList = nullptr;
|
|
if (NS_SUCCEEDED(UNWRAP_OBJECT(FileList, &obj, fileList))) {
|
|
const auto fileListStartIndex = cloneInfo->mFiles.Length();
|
|
const uint32_t fileListLength = fileList->Length();
|
|
|
|
if (size_t(fileListStartIndex) > size_t(UINT32_MAX) - fileListLength) {
|
|
MOZ_ASSERT(false,
|
|
"Fix the structured clone data to use a bigger type!");
|
|
return false;
|
|
}
|
|
|
|
if (!JS_WriteUint32Pair(aWriter, SCTAG_DOM_FILELIST, fileListLength)) {
|
|
return false;
|
|
}
|
|
|
|
for (uint32_t i = 0; i < fileList->Length(); ++i) {
|
|
File* file = fileList->Item(i);
|
|
if (!CopyingWriteBlob(aCx, aWriter, file, cloneInfo)) {
|
|
return false; // Everything should fail
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
{
|
|
Blob* blob = nullptr;
|
|
if (NS_SUCCEEDED(UNWRAP_OBJECT(Blob, &obj, blob))) {
|
|
return CopyingWriteBlob(aCx, aWriter, blob, cloneInfo);
|
|
}
|
|
}
|
|
|
|
return StructuredCloneHolder::WriteFullySerializableObjects(aCx, aWriter,
|
|
aObj);
|
|
}
|
|
|
|
void StructuredCloneErrorCallback(JSContext* aCx, uint32_t aErrorId,
|
|
void* aClosure, const char* aErrorMessage) {
|
|
// This callback is only used to prevent the default cloning TypeErrors
|
|
// from being thrown, as we will throw DataCloneErrors instead per spec.
|
|
}
|
|
|
|
nsresult GetAddInfoCallback(JSContext* aCx, void* aClosure) {
|
|
static const JSStructuredCloneCallbacks kStructuredCloneCallbacks = {
|
|
nullptr /* read */,
|
|
StructuredCloneWriteCallback /* write */,
|
|
StructuredCloneErrorCallback /* reportError */,
|
|
nullptr /* readTransfer */,
|
|
nullptr /* writeTransfer */,
|
|
nullptr /* freeTransfer */,
|
|
nullptr /* canTransfer */,
|
|
nullptr /* sabCloned */
|
|
};
|
|
|
|
MOZ_ASSERT(aCx);
|
|
|
|
auto* const data = static_cast<GetAddInfoClosure*>(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 <typename T>
|
|
JSObject* WrapAsJSObject(JSContext* const aCx, T& aBaseObject) {
|
|
JS::Rooted<JSObject*> result(aCx);
|
|
const bool res = WrapAsJSObject(aCx, aBaseObject, &result);
|
|
return res ? static_cast<JSObject*>(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_FILELIST) {
|
|
auto* const cloneInfo =
|
|
static_cast<IDBObjectStore::StructuredCloneInfo*>(aClosure);
|
|
|
|
// For empty filelist, aData is not used but must remain within bounds.
|
|
const auto& files = cloneInfo->mFiles;
|
|
const uint32_t fileListLength = aData;
|
|
|
|
if (fileListLength > files.Length()) {
|
|
MOZ_ASSERT(false, "Bad file list length value!");
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
// We need to ensure that all RAII smart pointers which may trigger GC are
|
|
// destroyed on return prior to this JS::Rooted being destroyed and
|
|
// unrooting the pointer. This scope helps make this intent more explicit.
|
|
JS::Rooted<JSObject*> obj(aCx);
|
|
{
|
|
nsCOMPtr<nsIGlobalObject> global = xpc::CurrentNativeGlobal(aCx);
|
|
if (!global) {
|
|
MOZ_ASSERT(false, "Could not access global!");
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
RefPtr<FileList> fileList = new FileList(global);
|
|
|
|
for (uint32_t i = 0u; i < fileListLength; ++i) {
|
|
uint32_t tag = UINT32_MAX;
|
|
uint32_t index = UINT32_MAX;
|
|
if (!JS_ReadUint32Pair(aReader, &tag, &index)) {
|
|
return nullptr;
|
|
}
|
|
|
|
const bool hasFileTag = tag == SCTAG_DOM_FILE;
|
|
if (!hasFileTag) {
|
|
MOZ_ASSERT(false, "Unexpected tag!");
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
if (uint64_t(index) >= cloneInfo->mFiles.Length()) {
|
|
MOZ_ASSERT(false, "Bad index!");
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
StructuredCloneFileChild& fileChild = cloneInfo->mFiles[index];
|
|
MOZ_ASSERT(fileChild.Type() == StructuredCloneFileBase::eBlob);
|
|
|
|
RefPtr<Blob> blob = fileChild.BlobPtr();
|
|
MOZ_ASSERT(blob->IsFile());
|
|
|
|
RefPtr<File> file = blob->ToFile();
|
|
if (!file) {
|
|
MOZ_ASSERT(false, "Could not convert blob to file!");
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
if (!fileList->Append(file)) {
|
|
MOZ_ASSERT(false, "Could not extend filelist!");
|
|
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
if (!WrapAsJSObject(aCx, fileList, &obj)) {
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
return obj;
|
|
}
|
|
|
|
if (aTag == SCTAG_DOM_BLOB || aTag == SCTAG_DOM_FILE ||
|
|
aTag == SCTAG_DOM_MUTABLEFILE) {
|
|
auto* const cloneInfo =
|
|
static_cast<IDBObjectStore::StructuredCloneInfo*>(aClosure);
|
|
|
|
if (aData >= cloneInfo->mFiles.Length()) {
|
|
MOZ_ASSERT(false, "Bad index value!");
|
|
return nullptr;
|
|
}
|
|
|
|
StructuredCloneFileChild& file = cloneInfo->mFiles[aData];
|
|
|
|
switch (static_cast<StructuredCloneTags>(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<JSObject*> result(aCx);
|
|
|
|
{
|
|
// Create a scope so ~RefPtr fires before returning an unwrapped
|
|
// JS::Value.
|
|
const RefPtr<Blob> blob = file.BlobPtr();
|
|
MOZ_ASSERT(blob->IsFile());
|
|
|
|
const RefPtr<File> 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 nullptr;
|
|
|
|
default:
|
|
// This cannot be reached due to the if condition before.
|
|
break;
|
|
}
|
|
}
|
|
|
|
return StructuredCloneHolder::ReadFullySerializableObjects(aCx, aReader, aTag,
|
|
true);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
const JSClass IDBObjectStore::sDummyPropJSClass = {
|
|
"IDBObjectStore Dummy", 0 /* flags */
|
|
};
|
|
|
|
IDBObjectStore::IDBObjectStore(SafeRefPtr<IDBTransaction> 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> IDBObjectStore::Create(
|
|
SafeRefPtr<IDBTransaction> 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<JS::Value> aVal,
|
|
nsTArray<IndexUpdateInfo>* const aUpdateInfoArray,
|
|
const VoidOrObjectStoreKeyPathString& aAutoIncrementedObjectStoreKeyPath,
|
|
ErrorResult* const aRv) {
|
|
// This precondition holds when `aVal` is the result of a structured clone.
|
|
js::AutoAssertNoContentJS noContentJS(aCx);
|
|
|
|
static_assert(std::is_same_v<IDBObjectStore::VoidOrObjectStoreKeyPathString,
|
|
KeyPath::VoidOrObjectStoreKeyPathString>,
|
|
"Inconsistent types");
|
|
|
|
if (!aMultiEntry) {
|
|
Key key;
|
|
*aRv =
|
|
aKeyPath.ExtractKey(aCx, aVal, key, aAutoIncrementedObjectStoreKeyPath);
|
|
|
|
// 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<JS::Value> 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<JSObject*> 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<JS::PropertyKey> 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<JS::Value> 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<JS::Value> 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<StructuredCloneReadInfoChild>,
|
|
nullptr,
|
|
StructuredCloneErrorCallback,
|
|
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<JS::Value> aKeyVal,
|
|
StructuredCloneWriteInfo& aCloneWriteInfo,
|
|
Key& aKey,
|
|
nsTArray<IndexUpdateInfo>& 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<NS_ERROR_DOM_INDEXEDDB_DATA_ERR>);
|
|
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<IndexMetadata>& indexes = mSpec->indexes();
|
|
const uint32_t idxCount = indexes.Length();
|
|
|
|
const auto& autoIncrementedObjectStoreKeyPath =
|
|
[this]() -> const nsAString& {
|
|
if (AutoIncrement() && GetKeyPath().IsValid()) {
|
|
// By https://w3c.github.io/IndexedDB/#database-interface ,
|
|
// createObjectStore algorithm, step 8, neither arrays nor empty paths
|
|
// are allowed for autoincremented object stores.
|
|
// See also KeyPath::IsAllowedForObjectStore.
|
|
MOZ_ASSERT(GetKeyPath().IsString());
|
|
MOZ_ASSERT(!GetKeyPath().IsEmpty());
|
|
return GetKeyPath().mStrings[0];
|
|
}
|
|
|
|
return VoidString();
|
|
}();
|
|
|
|
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,
|
|
autoIncrementedObjectStoreKeyPath, &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<IDBRequest> IDBObjectStore::AddOrPut(JSContext* aCx,
|
|
ValueWrapper& aValueWrapper,
|
|
JS::Handle<JS::Value> 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<IndexUpdateInfo> 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;
|
|
}
|
|
|
|
// Total structured clone size in bytes.
|
|
const size_t structuredCloneSize = cloneWriteInfo.mCloneBuffer.data().Size();
|
|
|
|
// Bug 1783242 introduced the use of shared memory for serializing large
|
|
// JSStructuredCloneData, which required adjusting IPC message size estimation
|
|
// calculations below. However, structured clone sizes are still limited:
|
|
// - On 32-bit platforms, crashes can occur around 2GB, and on 64-bit
|
|
// platforms around 4GB, even with shared memory.
|
|
// - Ideally, the content process would write directly to a file to avoid
|
|
// shared memory and data copying (see Bug 1944231).
|
|
//
|
|
// For now, structured clone sizes are capped at < 1042 MB (configurable via a
|
|
// preference).
|
|
if (structuredCloneSize > IndexedDatabaseManager::MaxStructuredCloneSize()) {
|
|
IDB_REPORT_INTERNAL_ERR();
|
|
aRv.ThrowUnknownError(nsPrintfCString(
|
|
"The structured clone is too large"
|
|
" (size=%zu bytes, max=%u bytes).",
|
|
structuredCloneSize, IndexedDatabaseManager::MaxStructuredCloneSize()));
|
|
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;
|
|
|
|
// Serialized structured clone size in bytes. For structured clone sizes >
|
|
// IPC::kMessageBufferShmemThreshold, only the size and shared memory handle
|
|
// are included in the IPC message. The value 16 is an estimate.
|
|
const size_t serializedStructuredCloneSize =
|
|
structuredCloneSize > IPC::kMessageBufferShmemThreshold
|
|
? 16
|
|
: structuredCloneSize;
|
|
|
|
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();
|
|
});
|
|
|
|
// TODO: Adjust the calculation of messageSize to account for the fallback
|
|
// to shared memory during serialization of the primary key and index keys if
|
|
// their size exceeds IPC::kMessageBufferShmemThreshold. This ensures the
|
|
// calculated size accurately reflects the actual IPC message size.
|
|
// See also bug 1945043.
|
|
const size_t messageSize = serializedStructuredCloneSize +
|
|
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 FileAddInfos.
|
|
QM_TRY_UNWRAP(
|
|
commonParams.fileAddInfos(),
|
|
TransformIntoNewArrayAbortOnErr(
|
|
cloneWriteInfo.mFiles,
|
|
[&database = *mTransaction->Database()](
|
|
auto& file) -> Result<FileAddInfo, nsresult> {
|
|
switch (file.Type()) {
|
|
case StructuredCloneFileBase::eBlob: {
|
|
MOZ_ASSERT(file.HasBlob());
|
|
|
|
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{WrapNotNull(fileActor),
|
|
StructuredCloneFileBase::eBlob};
|
|
}
|
|
|
|
case StructuredCloneFileBase::eWasmBytecode:
|
|
case StructuredCloneFileBase::eWasmCompiled: {
|
|
MOZ_ASSERT(file.HasBlob());
|
|
|
|
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{WrapNotNull(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<IDBRequest> IDBObjectStore::GetAllInternal(
|
|
bool aKeysOnly, JSContext* aCx, JS::Handle<JS::Value> aKey,
|
|
const Optional<uint32_t>& 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<IDBKeyRange> keyRange;
|
|
IDBKeyRange::FromJSVal(aCx, aKey, &keyRange, aRv);
|
|
if (NS_WARN_IF(aRv.Failed())) {
|
|
return nullptr;
|
|
}
|
|
|
|
const int64_t id = Id();
|
|
|
|
Maybe<SerializedKeyRange> 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<IDBRequest> IDBObjectStore::Add(JSContext* aCx,
|
|
JS::Handle<JS::Value> aValue,
|
|
JS::Handle<JS::Value> aKey,
|
|
ErrorResult& aRv) {
|
|
AssertIsOnOwningThread();
|
|
|
|
ValueWrapper valueWrapper(aCx, aValue);
|
|
|
|
return AddOrPut(aCx, valueWrapper, aKey, false, /* aFromCursor */ false, aRv);
|
|
}
|
|
|
|
RefPtr<IDBRequest> IDBObjectStore::Put(JSContext* aCx,
|
|
JS::Handle<JS::Value> aValue,
|
|
JS::Handle<JS::Value> aKey,
|
|
ErrorResult& aRv) {
|
|
AssertIsOnOwningThread();
|
|
|
|
ValueWrapper valueWrapper(aCx, aValue);
|
|
|
|
return AddOrPut(aCx, valueWrapper, aKey, true, /* aFromCursor */ false, aRv);
|
|
}
|
|
|
|
RefPtr<IDBRequest> IDBObjectStore::Delete(JSContext* aCx,
|
|
JS::Handle<JS::Value> aKey,
|
|
ErrorResult& aRv) {
|
|
AssertIsOnOwningThread();
|
|
|
|
return DeleteInternal(aCx, aKey, /* aFromCursor */ false, aRv);
|
|
}
|
|
|
|
RefPtr<IDBRequest> IDBObjectStore::Get(JSContext* aCx,
|
|
JS::Handle<JS::Value> aKey,
|
|
ErrorResult& aRv) {
|
|
AssertIsOnOwningThread();
|
|
|
|
return GetInternal(/* aKeyOnly */ false, aCx, aKey, aRv);
|
|
}
|
|
|
|
RefPtr<IDBRequest> IDBObjectStore::GetKey(JSContext* aCx,
|
|
JS::Handle<JS::Value> aKey,
|
|
ErrorResult& aRv) {
|
|
AssertIsOnOwningThread();
|
|
|
|
return GetInternal(/* aKeyOnly */ true, aCx, aKey, aRv);
|
|
}
|
|
|
|
RefPtr<IDBRequest> 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<IDBRequest> IDBObjectStore::GetAll(JSContext* aCx,
|
|
JS::Handle<JS::Value> aKey,
|
|
const Optional<uint32_t>& aLimit,
|
|
ErrorResult& aRv) {
|
|
AssertIsOnOwningThread();
|
|
|
|
return GetAllInternal(/* aKeysOnly */ false, aCx, aKey, aLimit, aRv);
|
|
}
|
|
|
|
RefPtr<IDBRequest> IDBObjectStore::GetAllKeys(JSContext* aCx,
|
|
JS::Handle<JS::Value> aKey,
|
|
const Optional<uint32_t>& aLimit,
|
|
ErrorResult& aRv) {
|
|
AssertIsOnOwningThread();
|
|
|
|
return GetAllInternal(/* aKeysOnly */ true, aCx, aKey, aLimit, aRv);
|
|
}
|
|
|
|
RefPtr<IDBRequest> IDBObjectStore::OpenCursor(JSContext* aCx,
|
|
JS::Handle<JS::Value> aRange,
|
|
IDBCursorDirection aDirection,
|
|
ErrorResult& aRv) {
|
|
AssertIsOnOwningThread();
|
|
|
|
return OpenCursorInternal(/* aKeysOnly */ false, aCx, aRange, aDirection,
|
|
aRv);
|
|
}
|
|
|
|
RefPtr<IDBRequest> IDBObjectStore::OpenCursor(JSContext* aCx,
|
|
IDBCursorDirection aDirection,
|
|
ErrorResult& aRv) {
|
|
AssertIsOnOwningThread();
|
|
|
|
return OpenCursorInternal(/* aKeysOnly */ false, aCx,
|
|
JS::UndefinedHandleValue, aDirection, aRv);
|
|
}
|
|
|
|
RefPtr<IDBRequest> IDBObjectStore::OpenKeyCursor(JSContext* aCx,
|
|
JS::Handle<JS::Value> aRange,
|
|
IDBCursorDirection aDirection,
|
|
ErrorResult& aRv) {
|
|
AssertIsOnOwningThread();
|
|
|
|
return OpenCursorInternal(/* aKeysOnly */ true, aCx, aRange, aDirection, aRv);
|
|
}
|
|
|
|
RefPtr<IDBIndex> IDBObjectStore::Index(const nsAString& aName,
|
|
ErrorResult& aRv) {
|
|
AssertIsOnOwningThread();
|
|
|
|
if (mTransaction->IsCommittingOrFinished() || mDeletedSpec) {
|
|
aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
|
|
return nullptr;
|
|
}
|
|
|
|
const nsTArray<IndexMetadata>& 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<IDBIndex> 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<JSObject*> aGivenProto) {
|
|
return IDBObjectStore_Binding::Wrap(aCx, this, aGivenProto);
|
|
}
|
|
|
|
nsIGlobalObject* IDBObjectStore::GetParentObject() const {
|
|
return mTransaction->GetParentObject();
|
|
}
|
|
|
|
void IDBObjectStore::GetKeyPath(JSContext* aCx,
|
|
JS::MutableHandle<JS::Value> 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<DOMStringList> IDBObjectStore::IndexNames() {
|
|
AssertIsOnOwningThread();
|
|
|
|
return CreateSortedDOMStringList(
|
|
mSpec->indexes(), [](const auto& index) { return index.name(); });
|
|
}
|
|
|
|
RefPtr<IDBRequest> IDBObjectStore::GetInternal(bool aKeyOnly, JSContext* aCx,
|
|
JS::Handle<JS::Value> 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<IDBKeyRange> 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<IDBRequest> IDBObjectStore::DeleteInternal(JSContext* aCx,
|
|
JS::Handle<JS::Value> 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<IDBKeyRange> 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<IDBIndex> 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<KeyPath, nsresult> {
|
|
if (!keyPath.IsValid()) {
|
|
return Err(NS_ERROR_DOM_SYNTAX_ERR);
|
|
}
|
|
|
|
return keyPath;
|
|
};
|
|
|
|
QM_INFOONLY_TRY_UNWRAP(
|
|
const auto maybeKeyPath,
|
|
([&aKeyPath, checkValid]() -> Result<KeyPath, nsresult> {
|
|
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();
|
|
}
|
|
|
|
if (!locale.IsEmpty()) {
|
|
// Set use counter and log deprecation warning for locale in parent doc.
|
|
nsIGlobalObject* global = GetParentObject();
|
|
AutoJSAPI jsapi;
|
|
// This isn't critical so don't error out if init fails.
|
|
if (jsapi.Init(global)) {
|
|
DeprecationWarning(
|
|
jsapi.cx(), global->GetGlobalJSObject(),
|
|
DeprecatedOperations::eIDBObjectStoreCreateIndexLocale);
|
|
}
|
|
}
|
|
|
|
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<IDBRequest> IDBObjectStore::Count(JSContext* aCx,
|
|
JS::Handle<JS::Value> 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<IDBKeyRange> 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<IDBRequest> IDBObjectStore::OpenCursorInternal(
|
|
bool aKeysOnly, JSContext* aCx, JS::Handle<JS::Value> 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<IDBKeyRange> keyRange;
|
|
IDBKeyRange::FromJSVal(aCx, aRange, &keyRange, aRv);
|
|
if (NS_WARN_IF(aRv.Failed())) {
|
|
return nullptr;
|
|
}
|
|
|
|
const int64_t objectStoreId = Id();
|
|
|
|
Maybe<SerializedKeyRange> 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<SafeRefPtr<BackgroundCursorChildBase>>(
|
|
MakeSafeRefPtr<
|
|
BackgroundCursorChild<IDBCursorType::ObjectStoreKey>>(
|
|
request, this, aDirection))
|
|
: MakeSafeRefPtr<BackgroundCursorChild<IDBCursorType::ObjectStore>>(
|
|
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<ObjectStoreSpec>(*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 */,
|
|
StructuredCloneErrorCallback /* reportError */,
|
|
nullptr /* readTransfer */,
|
|
nullptr /* writeTransfer */,
|
|
nullptr /* freeTransfer */,
|
|
nullptr /* canTransfer */,
|
|
nullptr /* sabCloned */
|
|
};
|
|
|
|
StructuredCloneInfo cloneInfo;
|
|
|
|
JS::Rooted<JS::Value> clonedValue(aCx);
|
|
if (!JS_StructuredClone(aCx, mValue, &clonedValue, &callbacks, &cloneInfo)) {
|
|
return false;
|
|
}
|
|
|
|
mValue = clonedValue;
|
|
|
|
mCloned = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
} // namespace mozilla::dom
|