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.cpp558
1 files changed, 558 insertions, 0 deletions
diff --git a/dom/indexedDB/KeyPath.cpp b/dom/indexedDB/KeyPath.cpp
new file mode 100644
index 0000000000..5cad164296
--- /dev/null
+++ b/dom/indexedDB/KeyPath.cpp
@@ -0,0 +1,558 @@
+/* -*- 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 {
+ 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;
+ }
+
+ 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