gecko-dev/dom/indexedDB/KeyPath.cpp

579 lines
15 KiB
C++

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=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 "Key.h"
#include "ReportInternalError.h"
#include "nsCharSeparatedTokenizer.h"
#include "nsJSUtils.h"
#include "xpcpublic.h"
#include "mozilla/dom/BindingDeclarations.h"
namespace mozilla {
namespace dom {
namespace indexedDB {
namespace {
inline
bool
IgnoreWhitespace(char16_t c)
{
return false;
}
typedef nsCharSeparatedTokenizerTemplate<IgnoreWhitespace> KeyPathTokenizer;
bool
IsValidKeyPathString(JSContext* aCx, const nsAString& aKeyPath)
{
NS_ASSERTION(!aKeyPath.IsVoid(), "What?");
KeyPathTokenizer tokenizer(aKeyPath, '.');
while (tokenizer.hasMoreTokens()) {
nsString token(tokenizer.nextToken());
if (!token.Length()) {
return false;
}
JS::Rooted<JS::Value> stringVal(aCx);
if (!xpc::StringToJsval(aCx, token, &stringVal)) {
return false;
}
NS_ASSERTION(stringVal.toString(), "This should never happen");
JS::Rooted<JSString*> str(aCx, stringVal.toString());
bool isIdentifier = false;
if (!JS_IsIdentifier(aCx, str, &isIdentifier) || !isIdentifier) {
return false;
}
}
// If the very last character was a '.', the tokenizer won't give us an empty
// token, but the keyPath is still invalid.
if (!aKeyPath.IsEmpty() &&
aKeyPath.CharAt(aKeyPath.Length() - 1) == '.') {
return false;
}
return true;
}
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(aCx, 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<JSObject*> obj(aCx,
aValue.isPrimitive() ? nullptr : aValue.toObjectOrNull());
while (tokenizer.hasMoreTokens()) {
const nsDependentSubstring& token = tokenizer.nextToken();
NS_ASSERTION(!token.IsEmpty(), "Should be a valid keypath");
const char16_t* keyPathChars = token.BeginReading();
const size_t keyPathLen = token.Length();
bool hasProp;
if (!targetObject) {
// We're still walking the chain of existing objects
if (!obj) {
return NS_ERROR_DOM_INDEXEDDB_DATA_ERR;
}
bool ok = JS_HasUCProperty(aCx, obj, keyPathChars, keyPathLen,
&hasProp);
IDB_ENSURE_TRUE(ok, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
if (hasProp) {
// Get if the property exists...
JS::Rooted<JS::Value> intermediate(aCx);
bool ok = JS_GetUCProperty(aCx, obj, keyPathChars, keyPathLen, &intermediate);
IDB_ENSURE_TRUE(ok, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
// Treat explicitly undefined as an error.
if (intermediate == JSVAL_VOID) {
return NS_ERROR_DOM_INDEXEDDB_DATA_ERR;
}
if (tokenizer.hasMoreTokens()) {
// ...and walk to it if there are more steps...
if (intermediate.isPrimitive()) {
return NS_ERROR_DOM_INDEXEDDB_DATA_ERR;
}
obj = intermediate.toObjectOrNull();
}
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 = JSVAL_VOID;
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_NewObject(aCx, nullptr, JS::NullPtr(),
JS::NullPtr()));
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(), JS::NullPtr(),
JS::NullPtr()));
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 :-(
bool succeeded;
if (!JS_DeleteUCProperty2(aCx, targetObject,
targetObjectPropName.get(),
targetObjectPropName.Length(),
&succeeded)) {
IDB_REPORT_INTERNAL_ERR();
return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
}
IDB_ENSURE_TRUE(succeeded, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
}
NS_ENSURE_SUCCESS(rv, rv);
return rv;
}
} // anonymous namespace
// static
nsresult
KeyPath::Parse(JSContext* aCx, const nsAString& aString, KeyPath* aKeyPath)
{
KeyPath keyPath(0);
keyPath.SetType(STRING);
if (!keyPath.AppendStringWithValidation(aCx, aString)) {
return NS_ERROR_FAILURE;
}
*aKeyPath = keyPath;
return NS_OK;
}
//static
nsresult
KeyPath::Parse(JSContext* aCx, const mozilla::dom::Sequence<nsString>& aStrings,
KeyPath* aKeyPath)
{
KeyPath keyPath(0);
keyPath.SetType(ARRAY);
for (uint32_t i = 0; i < aStrings.Length(); ++i) {
if (!keyPath.AppendStringWithValidation(aCx, aStrings[i])) {
return NS_ERROR_FAILURE;
}
}
*aKeyPath = keyPath;
return NS_OK;
}
// static
nsresult
KeyPath::Parse(JSContext* aCx, const JS::Value& aValue_, KeyPath* aKeyPath)
{
JS::Rooted<JS::Value> aValue(aCx, aValue_);
KeyPath keyPath(0);
aKeyPath->SetType(NONEXISTENT);
// See if this is a JS array.
if (JS_IsArrayObject(aCx, aValue)) {
JS::Rooted<JSObject*> obj(aCx, aValue.toObjectOrNull());
uint32_t length;
if (!JS_GetArrayLength(aCx, obj, &length)) {
return NS_ERROR_FAILURE;
}
if (!length) {
return NS_ERROR_FAILURE;
}
keyPath.SetType(ARRAY);
for (uint32_t index = 0; index < length; index++) {
JS::Rooted<JS::Value> val(aCx);
JSString* jsstr;
nsAutoJSString str;
if (!JS_GetElement(aCx, obj, index, &val) ||
!(jsstr = JS::ToString(aCx, val)) ||
!str.init(aCx, jsstr)) {
return NS_ERROR_FAILURE;
}
if (!keyPath.AppendStringWithValidation(aCx, str)) {
return NS_ERROR_FAILURE;
}
}
}
// Otherwise convert it to a string.
else if (!aValue.isNull() && !aValue.isUndefined()) {
JSString* jsstr;
nsAutoJSString str;
if (!(jsstr = JS::ToString(aCx, aValue)) ||
!str.init(aCx, jsstr)) {
return NS_ERROR_FAILURE;
}
keyPath.SetType(STRING);
if (!keyPath.AppendStringWithValidation(aCx, str)) {
return NS_ERROR_FAILURE;
}
}
*aKeyPath = keyPath;
return NS_OK;
}
void
KeyPath::SetType(KeyPathType aType)
{
mType = aType;
mStrings.Clear();
}
bool
KeyPath::AppendStringWithValidation(JSContext* aCx, const nsAString& aString)
{
if (!IsValidKeyPathString(aCx, 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;
}
NS_NOTREACHED("What?!");
return false;
}
nsresult
KeyPath::ExtractKey(JSContext* aCx, const JS::Value& aValue, Key& aKey) 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)) {
return rv;
}
if (NS_FAILED(aKey.AppendItem(aCx, IsArray() && i == 0, value))) {
NS_ASSERTION(aKey.IsUnset(), "Encoding error should unset");
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_SetElement(aCx, arrayObj, i, value)) {
IDB_REPORT_INTERNAL_ERR();
return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
}
}
*aOutVal = OBJECT_TO_JSVAL(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;
}
if (NS_FAILED(aKey.AppendItem(aCx, false, value))) {
NS_ASSERTION(aKey.IsUnset(), "Should be unset");
return value.isUndefined() ? NS_OK : NS_ERROR_DOM_INDEXEDDB_DATA_ERR;
}
aKey.FinishArray();
return NS_OK;
}
void
KeyPath::SerializeToString(nsAString& aString) const
{
NS_ASSERTION(IsValid(), "Check to see if I'm valid first!");
if (IsString()) {
aString = mStrings[0];
return;
}
if (IsArray()) {
// 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 :-)
uint32_t len = mStrings.Length();
for (uint32_t i = 0; i < len; ++i) {
aString.Append(',');
aString.Append(mStrings[i]);
}
return;
}
NS_NOTREACHED("What?");
}
// static
KeyPath
KeyPath::DeserializeFromString(const nsAString& aString)
{
KeyPath keyPath(0);
if (!aString.IsEmpty() && aString.First() == ',') {
keyPath.SetType(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<IgnoreWhitespace> tokenizer(aString, ',');
tokenizer.nextToken();
while (tokenizer.hasMoreTokens()) {
keyPath.mStrings.AppendElement(tokenizer.nextToken());
}
return keyPath;
}
keyPath.SetType(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_SetElement(aCx, array, i, val)) {
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 indexedDB
} // namespace dom
} // namespace mozilla