gecko-dev/dom/indexedDB/KeyPath.cpp
2024-04-08 15:25:27 +00:00

571 lines
17 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 "KeyPath.h"
#include "IDBObjectStore.h"
#include "IndexedDBCommon.h"
#include "Key.h"
#include "ReportInternalError.h"
#include "js/Array.h" // JS::NewArrayObject
#include "js/PropertyAndElement.h" // JS_DefineElement, JS_DefineUCProperty, JS_DeleteUCProperty
#include "js/PropertyDescriptor.h" // JS::PropertyDescriptor, JS_GetOwnUCPropertyDescriptor
#include "mozilla/ResultExtensions.h"
#include "mozilla/dom/BindingDeclarations.h"
#include "mozilla/dom/Blob.h"
#include "mozilla/dom/BlobBinding.h"
#include "mozilla/dom/File.h"
#include "mozilla/dom/IDBObjectStoreBinding.h"
#include "mozilla/dom/quota/ResultExtensions.h"
#include "nsCharSeparatedTokenizer.h"
#include "nsJSUtils.h"
#include "nsPrintfCString.h"
#include "xpcpublic.h"
namespace mozilla::dom::indexedDB {
namespace {
using KeyPathTokenizer =
nsCharSeparatedTokenizerTemplate<NS_TokenizerIgnoreNothing>;
bool IsValidKeyPathString(const nsAString& aKeyPath) {
NS_ASSERTION(!aKeyPath.IsVoid(), "What?");
for (const auto& token : KeyPathTokenizer(aKeyPath, '.').ToRange()) {
if (token.IsEmpty()) {
return false;
}
if (!JS_IsIdentifier(token.Data(), token.Length())) {
return false;
}
}
// If the very last character was a '.', the tokenizer won't give us an empty
// token, but the keyPath is still invalid.
return aKeyPath.IsEmpty() || aKeyPath.CharAt(aKeyPath.Length() - 1) != '.';
}
enum KeyExtractionOptions { DoNotCreateProperties, CreateProperties };
nsresult GetJSValFromKeyPathString(
JSContext* aCx, const JS::Value& aValue, const nsAString& aKeyPathString,
JS::Value* aKeyJSVal, KeyExtractionOptions aOptions,
KeyPath::ExtractOrCreateKeyCallback aCallback, void* aClosure) {
NS_ASSERTION(aCx, "Null pointer!");
NS_ASSERTION(IsValidKeyPathString(aKeyPathString), "This will explode!");
NS_ASSERTION(!(aCallback || aClosure) || aOptions == CreateProperties,
"This is not allowed!");
NS_ASSERTION(aOptions != CreateProperties || aCallback,
"If properties are created, there must be a callback!");
nsresult rv = NS_OK;
*aKeyJSVal = aValue;
KeyPathTokenizer tokenizer(aKeyPathString, '.');
nsString targetObjectPropName;
JS::Rooted<JSObject*> targetObject(aCx, nullptr);
JS::Rooted<JS::Value> currentVal(aCx, aValue);
JS::Rooted<JSObject*> obj(aCx);
while (tokenizer.hasMoreTokens()) {
const auto& token = tokenizer.nextToken();
NS_ASSERTION(!token.IsEmpty(), "Should be a valid keypath");
const char16_t* keyPathChars = token.BeginReading();
const size_t keyPathLen = token.Length();
if (!targetObject) {
// We're still walking the chain of existing objects
// http://w3c.github.io/IndexedDB/#evaluate-a-key-path-on-a-value
// step 4 substep 1: check for .length on a String value.
if (currentVal.isString() && !tokenizer.hasMoreTokens() &&
token.EqualsLiteral("length")) {
aKeyJSVal->setNumber(JS_GetStringLength(currentVal.toString()));
break;
}
if (!currentVal.isObject()) {
return NS_ERROR_DOM_INDEXEDDB_DATA_ERR;
}
obj = &currentVal.toObject();
// We call JS_GetOwnUCPropertyDescriptor on purpose (as opposed to
// JS_GetUCPropertyDescriptor) to avoid searching the prototype chain.
JS::Rooted<mozilla::Maybe<JS::PropertyDescriptor>> desc(aCx);
QM_TRY(OkIf(JS_GetOwnUCPropertyDescriptor(aCx, obj, keyPathChars,
keyPathLen, &desc)),
NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR,
IDB_REPORT_INTERNAL_ERR_LAMBDA);
JS::Rooted<JS::Value> intermediate(aCx);
bool hasProp = false;
if (desc.isSome() && desc->isDataDescriptor()) {
intermediate = desc->value();
hasProp = true;
} else {
// If we get here it means the object doesn't have the property or the
// property is available throuch a getter. We don't want to call any
// getters to avoid potential re-entrancy.
// The blob object is special since its properties are available
// only through getters but we still want to support them for key
// extraction. So they need to be handled manually.
Blob* blob;
if (NS_SUCCEEDED(UNWRAP_OBJECT(Blob, &obj, blob))) {
if (token.EqualsLiteral("size")) {
ErrorResult rv;
uint64_t size = blob->GetSize(rv);
MOZ_ALWAYS_TRUE(!rv.Failed());
intermediate = JS_NumberValue(size);
hasProp = true;
} else if (token.EqualsLiteral("type")) {
nsString type;
blob->GetType(type);
JSString* string =
JS_NewUCStringCopyN(aCx, type.get(), type.Length());
intermediate = JS::StringValue(string);
hasProp = true;
} else {
RefPtr<File> file = blob->ToFile();
if (file) {
if (token.EqualsLiteral("name")) {
nsString name;
file->GetName(name);
JSString* string =
JS_NewUCStringCopyN(aCx, name.get(), name.Length());
intermediate = JS::StringValue(string);
hasProp = true;
} else if (token.EqualsLiteral("lastModified")) {
ErrorResult rv;
int64_t lastModifiedDate = file->GetLastModified(rv);
MOZ_ALWAYS_TRUE(!rv.Failed());
intermediate = JS_NumberValue(lastModifiedDate);
hasProp = true;
}
// The spec also lists "lastModifiedDate", but we deprecated and
// removed support for it.
}
}
}
}
if (hasProp) {
// Treat explicitly undefined as an error.
if (intermediate.isUndefined()) {
return NS_ERROR_DOM_INDEXEDDB_DATA_ERR;
}
if (tokenizer.hasMoreTokens()) {
// ...and walk to it if there are more steps...
currentVal = intermediate;
} else {
// ...otherwise use it as key
*aKeyJSVal = intermediate;
}
} else {
// If the property doesn't exist, fall into below path of starting
// to define properties, if allowed.
if (aOptions == DoNotCreateProperties) {
return NS_ERROR_DOM_INDEXEDDB_DATA_ERR;
}
targetObject = obj;
targetObjectPropName = token;
}
}
if (targetObject) {
// We have started inserting new objects or are about to just insert
// the first one.
aKeyJSVal->setUndefined();
if (tokenizer.hasMoreTokens()) {
// If we're not at the end, we need to add a dummy object to the
// chain.
JS::Rooted<JSObject*> dummy(aCx, JS_NewPlainObject(aCx));
if (!dummy) {
IDB_REPORT_INTERNAL_ERR();
rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
break;
}
if (!JS_DefineUCProperty(aCx, obj, token.BeginReading(), token.Length(),
dummy, JSPROP_ENUMERATE)) {
IDB_REPORT_INTERNAL_ERR();
rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
break;
}
obj = dummy;
} else {
JS::Rooted<JSObject*> dummy(
aCx, JS_NewObject(aCx, IDBObjectStore::DummyPropClass()));
if (!dummy) {
IDB_REPORT_INTERNAL_ERR();
rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
break;
}
if (!JS_DefineUCProperty(aCx, obj, token.BeginReading(), token.Length(),
dummy, JSPROP_ENUMERATE)) {
IDB_REPORT_INTERNAL_ERR();
rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
break;
}
obj = dummy;
}
}
}
// We guard on rv being a success because we need to run the property
// deletion code below even if we should not be running the callback.
if (NS_SUCCEEDED(rv) && aCallback) {
rv = (*aCallback)(aCx, aClosure);
}
if (targetObject) {
// If this fails, we lose, and the web page sees a magical property
// appear on the object :-(
JS::ObjectOpResult succeeded;
if (!JS_DeleteUCProperty(aCx, targetObject, targetObjectPropName.get(),
targetObjectPropName.Length(), succeeded)) {
IDB_REPORT_INTERNAL_ERR();
return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
}
QM_TRY(OkIf(succeeded.ok()), NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR,
IDB_REPORT_INTERNAL_ERR_LAMBDA);
}
// TODO: It would be nicer to do the cleanup using a RAII class or something.
// This last QM_TRY could be removed then.
QM_TRY(MOZ_TO_RESULT(rv));
return NS_OK;
}
} // namespace
// static
Result<KeyPath, nsresult> KeyPath::Parse(const nsAString& aString) {
KeyPath keyPath(0);
keyPath.SetType(KeyPathType::String);
if (!keyPath.AppendStringWithValidation(aString)) {
return Err(NS_ERROR_FAILURE);
}
return keyPath;
}
// static
Result<KeyPath, nsresult> KeyPath::Parse(const Sequence<nsString>& aStrings) {
KeyPath keyPath(0);
keyPath.SetType(KeyPathType::Array);
for (uint32_t i = 0; i < aStrings.Length(); ++i) {
if (!keyPath.AppendStringWithValidation(aStrings[i])) {
return Err(NS_ERROR_FAILURE);
}
}
return keyPath;
}
// static
Result<KeyPath, nsresult> KeyPath::Parse(
const Nullable<OwningStringOrStringSequence>& aValue) {
if (aValue.IsNull()) {
return KeyPath{0};
}
if (aValue.Value().IsString()) {
return Parse(aValue.Value().GetAsString());
}
MOZ_ASSERT(aValue.Value().IsStringSequence());
const Sequence<nsString>& seq = aValue.Value().GetAsStringSequence();
if (seq.Length() == 0) {
return Err(NS_ERROR_FAILURE);
}
return Parse(seq);
}
void KeyPath::SetType(KeyPathType aType) {
mType = aType;
mStrings.Clear();
}
bool KeyPath::AppendStringWithValidation(const nsAString& aString) {
if (!IsValidKeyPathString(aString)) {
return false;
}
if (IsString()) {
NS_ASSERTION(mStrings.Length() == 0, "Too many strings!");
mStrings.AppendElement(aString);
return true;
}
if (IsArray()) {
mStrings.AppendElement(aString);
return true;
}
MOZ_ASSERT_UNREACHABLE("What?!");
return false;
}
nsresult KeyPath::ExtractKey(JSContext* aCx, const JS::Value& aValue, Key& aKey,
const VoidOrObjectStoreKeyPathString&
aAutoIncrementedObjectStoreKeyPath) const {
uint32_t len = mStrings.Length();
JS::Rooted<JS::Value> value(aCx);
aKey.Unset();
for (uint32_t i = 0; i < len; ++i) {
nsresult rv =
GetJSValFromKeyPathString(aCx, aValue, mStrings[i], value.address(),
DoNotCreateProperties, nullptr, nullptr);
if (NS_FAILED(rv)) {
if (!aAutoIncrementedObjectStoreKeyPath.IsVoid() &&
mStrings[i].Equals(aAutoIncrementedObjectStoreKeyPath)) {
// We are extracting index keys of an object to be added if
// object store key path for a string key is provided.
// Because the autoIncrement primary key is part of
// this index key but is not defined in |aValue|, so we reserve
// the space here to update the key later in parent.
aKey.ReserveAutoIncrementKey(IsArray() && i == 0);
continue;
}
return rv;
}
auto result = aKey.AppendItem(aCx, IsArray() && i == 0, value);
if (result.isErr()) {
NS_ASSERTION(aKey.IsUnset(), "Encoding error should unset");
if (result.inspectErr().Is(SpecialValues::Exception)) {
result.unwrapErr().AsException().SuppressException();
}
return NS_ERROR_DOM_INDEXEDDB_DATA_ERR;
}
}
aKey.FinishArray();
return NS_OK;
}
nsresult KeyPath::ExtractKeyAsJSVal(JSContext* aCx, const JS::Value& aValue,
JS::Value* aOutVal) const {
NS_ASSERTION(IsValid(), "This doesn't make sense!");
if (IsString()) {
return GetJSValFromKeyPathString(aCx, aValue, mStrings[0], aOutVal,
DoNotCreateProperties, nullptr, nullptr);
}
const uint32_t len = mStrings.Length();
JS::Rooted<JSObject*> arrayObj(aCx, JS::NewArrayObject(aCx, len));
if (!arrayObj) {
return NS_ERROR_OUT_OF_MEMORY;
}
JS::Rooted<JS::Value> value(aCx);
for (uint32_t i = 0; i < len; ++i) {
nsresult rv =
GetJSValFromKeyPathString(aCx, aValue, mStrings[i], value.address(),
DoNotCreateProperties, nullptr, nullptr);
if (NS_FAILED(rv)) {
return rv;
}
if (!JS_DefineElement(aCx, arrayObj, i, value, JSPROP_ENUMERATE)) {
IDB_REPORT_INTERNAL_ERR();
return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
}
}
aOutVal->setObject(*arrayObj);
return NS_OK;
}
nsresult KeyPath::ExtractOrCreateKey(JSContext* aCx, const JS::Value& aValue,
Key& aKey,
ExtractOrCreateKeyCallback aCallback,
void* aClosure) const {
NS_ASSERTION(IsString(), "This doesn't make sense!");
JS::Rooted<JS::Value> value(aCx);
aKey.Unset();
nsresult rv =
GetJSValFromKeyPathString(aCx, aValue, mStrings[0], value.address(),
CreateProperties, aCallback, aClosure);
if (NS_FAILED(rv)) {
return rv;
}
auto result = aKey.AppendItem(aCx, false, value);
if (result.isErr()) {
NS_ASSERTION(aKey.IsUnset(), "Should be unset");
if (result.inspectErr().Is(SpecialValues::Exception)) {
result.unwrapErr().AsException().SuppressException();
}
return value.isUndefined() ? NS_OK : NS_ERROR_DOM_INDEXEDDB_DATA_ERR;
}
aKey.FinishArray();
return NS_OK;
}
nsAutoString KeyPath::SerializeToString() const {
NS_ASSERTION(IsValid(), "Check to see if I'm valid first!");
if (IsString()) {
return nsAutoString{mStrings[0]};
}
if (IsArray()) {
nsAutoString res;
// We use a comma in the beginning to indicate that it's an array of
// key paths. This is to be able to tell a string-keypath from an
// array-keypath which contains only one item.
// It also makes serializing easier :-)
const uint32_t len = mStrings.Length();
for (uint32_t i = 0; i < len; ++i) {
res.Append(',');
res.Append(mStrings[i]);
}
return res;
}
MOZ_ASSERT_UNREACHABLE("What?");
return {};
}
// static
KeyPath KeyPath::DeserializeFromString(const nsAString& aString) {
KeyPath keyPath(0);
if (!aString.IsEmpty() && aString.First() == ',') {
keyPath.SetType(KeyPathType::Array);
// We use a comma in the beginning to indicate that it's an array of
// key paths. This is to be able to tell a string-keypath from an
// array-keypath which contains only one item.
nsCharSeparatedTokenizerTemplate<NS_TokenizerIgnoreNothing> tokenizer(
aString, ',');
tokenizer.nextToken();
while (tokenizer.hasMoreTokens()) {
keyPath.mStrings.AppendElement(tokenizer.nextToken());
}
if (tokenizer.separatorAfterCurrentToken()) {
// There is a trailing comma, indicating the original KeyPath has
// a trailing empty string, i.e. [..., '']. We should append this
// empty string.
keyPath.mStrings.EmplaceBack();
}
return keyPath;
}
keyPath.SetType(KeyPathType::String);
keyPath.mStrings.AppendElement(aString);
return keyPath;
}
nsresult KeyPath::ToJSVal(JSContext* aCx,
JS::MutableHandle<JS::Value> aValue) const {
if (IsArray()) {
uint32_t len = mStrings.Length();
JS::Rooted<JSObject*> array(aCx, JS::NewArrayObject(aCx, len));
if (!array) {
IDB_WARNING("Failed to make array!");
return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
}
for (uint32_t i = 0; i < len; ++i) {
JS::Rooted<JS::Value> val(aCx);
nsString tmp(mStrings[i]);
if (!xpc::StringToJsval(aCx, tmp, &val)) {
IDB_REPORT_INTERNAL_ERR();
return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
}
if (!JS_DefineElement(aCx, array, i, val, JSPROP_ENUMERATE)) {
IDB_REPORT_INTERNAL_ERR();
return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
}
}
aValue.setObject(*array);
return NS_OK;
}
if (IsString()) {
nsString tmp(mStrings[0]);
if (!xpc::StringToJsval(aCx, tmp, aValue)) {
IDB_REPORT_INTERNAL_ERR();
return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
}
return NS_OK;
}
aValue.setNull();
return NS_OK;
}
nsresult KeyPath::ToJSVal(JSContext* aCx, JS::Heap<JS::Value>& aValue) const {
JS::Rooted<JS::Value> value(aCx);
nsresult rv = ToJSVal(aCx, &value);
if (NS_SUCCEEDED(rv)) {
aValue = value;
}
return rv;
}
bool KeyPath::IsAllowedForObjectStore(bool aAutoIncrement) const {
// Any keypath that passed validation is allowed for non-autoIncrement
// objectStores.
if (!aAutoIncrement) {
return true;
}
// Array keypaths are not allowed for autoIncrement objectStores.
if (IsArray()) {
return false;
}
// Neither are empty strings.
if (IsEmpty()) {
return false;
}
// Everything else is ok.
return true;
}
} // namespace mozilla::dom::indexedDB