summaryrefslogtreecommitdiffstats
path: root/dom/indexedDB/KeyPath.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'dom/indexedDB/KeyPath.cpp')
-rw-r--r--dom/indexedDB/KeyPath.cpp539
1 files changed, 539 insertions, 0 deletions
diff --git a/dom/indexedDB/KeyPath.cpp b/dom/indexedDB/KeyPath.cpp
new file mode 100644
index 000000000..dc8d10668
--- /dev/null
+++ b/dom/indexedDB/KeyPath.cpp
@@ -0,0 +1,539 @@
+/* -*- 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 "Key.h"
+#include "ReportInternalError.h"
+
+#include "nsCharSeparatedTokenizer.h"
+#include "nsJSUtils.h"
+#include "xpcpublic.h"
+
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/IDBObjectStoreBinding.h"
+
+namespace mozilla {
+namespace dom {
+namespace indexedDB {
+
+namespace {
+
+inline
+bool
+IgnoreWhitespace(char16_t c)
+{
+ return false;
+}
+
+typedef nsCharSeparatedTokenizerTemplate<IgnoreWhitespace> KeyPathTokenizer;
+
+bool
+IsValidKeyPathString(const nsAString& aKeyPath)
+{
+ NS_ASSERTION(!aKeyPath.IsVoid(), "What?");
+
+ KeyPathTokenizer tokenizer(aKeyPath, '.');
+
+ while (tokenizer.hasMoreTokens()) {
+ nsString token(tokenizer.nextToken());
+
+ if (!token.Length()) {
+ return false;
+ }
+
+ if (!JS_IsIdentifier(token.get(), 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.
+ 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(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 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
+ // http://w3c.github.io/IndexedDB/#dfn-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") && aOptions == DoNotCreateProperties) {
+ aKeyJSVal->setNumber(double(JS_GetStringLength(currentVal.toString())));
+ break;
+ }
+
+ if (!currentVal.isObject()) {
+ return NS_ERROR_DOM_INDEXEDDB_DATA_ERR;
+ }
+ obj = &currentVal.toObject();
+
+ 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.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;
+ }
+ IDB_ENSURE_TRUE(succeeded, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
+ }
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ return rv;
+}
+
+} // namespace
+
+// static
+nsresult
+KeyPath::Parse(const nsAString& aString, KeyPath* aKeyPath)
+{
+ KeyPath keyPath(0);
+ keyPath.SetType(STRING);
+
+ if (!keyPath.AppendStringWithValidation(aString)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ *aKeyPath = keyPath;
+ return NS_OK;
+}
+
+//static
+nsresult
+KeyPath::Parse(const Sequence<nsString>& aStrings, KeyPath* aKeyPath)
+{
+ KeyPath keyPath(0);
+ keyPath.SetType(ARRAY);
+
+ for (uint32_t i = 0; i < aStrings.Length(); ++i) {
+ if (!keyPath.AppendStringWithValidation(aStrings[i])) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ *aKeyPath = keyPath;
+ return NS_OK;
+}
+
+// static
+nsresult
+KeyPath::Parse(const Nullable<OwningStringOrStringSequence>& aValue, KeyPath* aKeyPath)
+{
+ KeyPath keyPath(0);
+
+ aKeyPath->SetType(NONEXISTENT);
+
+ if (aValue.IsNull()) {
+ *aKeyPath = keyPath;
+ return NS_OK;
+ }
+
+ if (aValue.Value().IsString()) {
+ return Parse(aValue.Value().GetAsString(), aKeyPath);
+ }
+
+ MOZ_ASSERT(aValue.Value().IsStringSequence());
+
+ const Sequence<nsString>& seq = aValue.Value().GetAsStringSequence();
+ if (seq.Length() == 0) {
+ return NS_ERROR_FAILURE;
+ }
+ return Parse(seq, aKeyPath);
+}
+
+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;
+ }
+
+ 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_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;
+ }
+
+ 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_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 indexedDB
+} // namespace dom
+} // namespace mozilla