Source code

Revision control

Copy as Markdown

Other Tools

/* -*- 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()) {
// 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;
// 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 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