Bug 1755316 - Add audio tests with simultaneous processes r=alwu
[gecko.git] / dom / indexedDB / KeyPath.cpp
blobd96e0bbd423ed58a632c42aff8f11bee050ef616
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "KeyPath.h"
9 #include "IDBObjectStore.h"
10 #include "IndexedDBCommon.h"
11 #include "Key.h"
12 #include "ReportInternalError.h"
13 #include "js/Array.h" // JS::NewArrayObject
14 #include "js/PropertyAndElement.h" // JS_DefineElement, JS_DefineUCProperty, JS_DeleteUCProperty
15 #include "js/PropertyDescriptor.h" // JS::PropertyDescriptor, JS_GetOwnUCPropertyDescriptor
16 #include "mozilla/ResultExtensions.h"
17 #include "mozilla/dom/BindingDeclarations.h"
18 #include "mozilla/dom/Blob.h"
19 #include "mozilla/dom/BlobBinding.h"
20 #include "mozilla/dom/File.h"
21 #include "mozilla/dom/IDBObjectStoreBinding.h"
22 #include "mozilla/dom/quota/ResultExtensions.h"
23 #include "nsCharSeparatedTokenizer.h"
24 #include "nsJSUtils.h"
25 #include "nsPrintfCString.h"
26 #include "xpcpublic.h"
28 namespace mozilla::dom::indexedDB {
30 namespace {
32 using KeyPathTokenizer =
33 nsCharSeparatedTokenizerTemplate<NS_TokenizerIgnoreNothing>;
35 bool IsValidKeyPathString(const nsAString& aKeyPath) {
36 NS_ASSERTION(!aKeyPath.IsVoid(), "What?");
38 for (const auto& token : KeyPathTokenizer(aKeyPath, '.').ToRange()) {
39 if (token.IsEmpty()) {
40 return false;
43 if (!JS_IsIdentifier(token.Data(), token.Length())) {
44 return false;
48 // If the very last character was a '.', the tokenizer won't give us an empty
49 // token, but the keyPath is still invalid.
50 return aKeyPath.IsEmpty() || aKeyPath.CharAt(aKeyPath.Length() - 1) != '.';
53 enum KeyExtractionOptions { DoNotCreateProperties, CreateProperties };
55 nsresult GetJSValFromKeyPathString(
56 JSContext* aCx, const JS::Value& aValue, const nsAString& aKeyPathString,
57 JS::Value* aKeyJSVal, KeyExtractionOptions aOptions,
58 KeyPath::ExtractOrCreateKeyCallback aCallback, void* aClosure) {
59 NS_ASSERTION(aCx, "Null pointer!");
60 NS_ASSERTION(IsValidKeyPathString(aKeyPathString), "This will explode!");
61 NS_ASSERTION(!(aCallback || aClosure) || aOptions == CreateProperties,
62 "This is not allowed!");
63 NS_ASSERTION(aOptions != CreateProperties || aCallback,
64 "If properties are created, there must be a callback!");
66 nsresult rv = NS_OK;
67 *aKeyJSVal = aValue;
69 KeyPathTokenizer tokenizer(aKeyPathString, '.');
71 nsString targetObjectPropName;
72 JS::Rooted<JSObject*> targetObject(aCx, nullptr);
73 JS::Rooted<JS::Value> currentVal(aCx, aValue);
74 JS::Rooted<JSObject*> obj(aCx);
76 while (tokenizer.hasMoreTokens()) {
77 const auto& token = tokenizer.nextToken();
79 NS_ASSERTION(!token.IsEmpty(), "Should be a valid keypath");
81 const char16_t* keyPathChars = token.BeginReading();
82 const size_t keyPathLen = token.Length();
84 if (!targetObject) {
85 // We're still walking the chain of existing objects
86 // http://w3c.github.io/IndexedDB/#evaluate-a-key-path-on-a-value
87 // step 4 substep 1: check for .length on a String value.
88 if (currentVal.isString() && !tokenizer.hasMoreTokens() &&
89 token.EqualsLiteral("length")) {
90 aKeyJSVal->setNumber(double(JS_GetStringLength(currentVal.toString())));
91 break;
94 if (!currentVal.isObject()) {
95 return NS_ERROR_DOM_INDEXEDDB_DATA_ERR;
97 obj = &currentVal.toObject();
99 // We call JS_GetOwnUCPropertyDescriptor on purpose (as opposed to
100 // JS_GetUCPropertyDescriptor) to avoid searching the prototype chain.
101 JS::Rooted<mozilla::Maybe<JS::PropertyDescriptor>> desc(aCx);
102 QM_TRY(OkIf(JS_GetOwnUCPropertyDescriptor(aCx, obj, keyPathChars,
103 keyPathLen, &desc)),
104 NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR,
105 IDB_REPORT_INTERNAL_ERR_LAMBDA);
107 JS::Rooted<JS::Value> intermediate(aCx);
108 bool hasProp = false;
110 if (desc.isSome() && desc->isDataDescriptor()) {
111 intermediate = desc->value();
112 hasProp = true;
113 } else {
114 // If we get here it means the object doesn't have the property or the
115 // property is available throuch a getter. We don't want to call any
116 // getters to avoid potential re-entrancy.
117 // The blob object is special since its properties are available
118 // only through getters but we still want to support them for key
119 // extraction. So they need to be handled manually.
120 Blob* blob;
121 if (NS_SUCCEEDED(UNWRAP_OBJECT(Blob, &obj, blob))) {
122 if (token.EqualsLiteral("size")) {
123 ErrorResult rv;
124 uint64_t size = blob->GetSize(rv);
125 MOZ_ALWAYS_TRUE(!rv.Failed());
127 intermediate = JS_NumberValue(size);
128 hasProp = true;
129 } else if (token.EqualsLiteral("type")) {
130 nsString type;
131 blob->GetType(type);
133 JSString* string =
134 JS_NewUCStringCopyN(aCx, type.get(), type.Length());
136 intermediate = JS::StringValue(string);
137 hasProp = true;
138 } else {
139 RefPtr<File> file = blob->ToFile();
140 if (file) {
141 if (token.EqualsLiteral("name")) {
142 nsString name;
143 file->GetName(name);
145 JSString* string =
146 JS_NewUCStringCopyN(aCx, name.get(), name.Length());
148 intermediate = JS::StringValue(string);
149 hasProp = true;
150 } else if (token.EqualsLiteral("lastModified")) {
151 ErrorResult rv;
152 int64_t lastModifiedDate = file->GetLastModified(rv);
153 MOZ_ALWAYS_TRUE(!rv.Failed());
155 intermediate = JS_NumberValue(lastModifiedDate);
156 hasProp = true;
158 // The spec also lists "lastModifiedDate", but we deprecated and
159 // removed support for it.
165 if (hasProp) {
166 // Treat explicitly undefined as an error.
167 if (intermediate.isUndefined()) {
168 return NS_ERROR_DOM_INDEXEDDB_DATA_ERR;
170 if (tokenizer.hasMoreTokens()) {
171 // ...and walk to it if there are more steps...
172 currentVal = intermediate;
173 } else {
174 // ...otherwise use it as key
175 *aKeyJSVal = intermediate;
177 } else {
178 // If the property doesn't exist, fall into below path of starting
179 // to define properties, if allowed.
180 if (aOptions == DoNotCreateProperties) {
181 return NS_ERROR_DOM_INDEXEDDB_DATA_ERR;
184 targetObject = obj;
185 targetObjectPropName = token;
189 if (targetObject) {
190 // We have started inserting new objects or are about to just insert
191 // the first one.
193 aKeyJSVal->setUndefined();
195 if (tokenizer.hasMoreTokens()) {
196 // If we're not at the end, we need to add a dummy object to the
197 // chain.
198 JS::Rooted<JSObject*> dummy(aCx, JS_NewPlainObject(aCx));
199 if (!dummy) {
200 IDB_REPORT_INTERNAL_ERR();
201 rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
202 break;
205 if (!JS_DefineUCProperty(aCx, obj, token.BeginReading(), token.Length(),
206 dummy, JSPROP_ENUMERATE)) {
207 IDB_REPORT_INTERNAL_ERR();
208 rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
209 break;
212 obj = dummy;
213 } else {
214 JS::Rooted<JSObject*> dummy(
215 aCx, JS_NewObject(aCx, IDBObjectStore::DummyPropClass()));
216 if (!dummy) {
217 IDB_REPORT_INTERNAL_ERR();
218 rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
219 break;
222 if (!JS_DefineUCProperty(aCx, obj, token.BeginReading(), token.Length(),
223 dummy, JSPROP_ENUMERATE)) {
224 IDB_REPORT_INTERNAL_ERR();
225 rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
226 break;
229 obj = dummy;
234 // We guard on rv being a success because we need to run the property
235 // deletion code below even if we should not be running the callback.
236 if (NS_SUCCEEDED(rv) && aCallback) {
237 rv = (*aCallback)(aCx, aClosure);
240 if (targetObject) {
241 // If this fails, we lose, and the web page sees a magical property
242 // appear on the object :-(
243 JS::ObjectOpResult succeeded;
244 if (!JS_DeleteUCProperty(aCx, targetObject, targetObjectPropName.get(),
245 targetObjectPropName.Length(), succeeded)) {
246 IDB_REPORT_INTERNAL_ERR();
247 return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
249 QM_TRY(OkIf(succeeded.ok()), NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR,
250 IDB_REPORT_INTERNAL_ERR_LAMBDA);
253 // TODO: It would be nicer to do the cleanup using a RAII class or something.
254 // This last QM_TRY could be removed then.
255 QM_TRY(MOZ_TO_RESULT(rv));
256 return NS_OK;
259 } // namespace
261 // static
262 Result<KeyPath, nsresult> KeyPath::Parse(const nsAString& aString) {
263 KeyPath keyPath(0);
264 keyPath.SetType(KeyPathType::String);
266 if (!keyPath.AppendStringWithValidation(aString)) {
267 return Err(NS_ERROR_FAILURE);
270 return keyPath;
273 // static
274 Result<KeyPath, nsresult> KeyPath::Parse(const Sequence<nsString>& aStrings) {
275 KeyPath keyPath(0);
276 keyPath.SetType(KeyPathType::Array);
278 for (uint32_t i = 0; i < aStrings.Length(); ++i) {
279 if (!keyPath.AppendStringWithValidation(aStrings[i])) {
280 return Err(NS_ERROR_FAILURE);
284 return keyPath;
287 // static
288 Result<KeyPath, nsresult> KeyPath::Parse(
289 const Nullable<OwningStringOrStringSequence>& aValue) {
290 if (aValue.IsNull()) {
291 return KeyPath{0};
294 if (aValue.Value().IsString()) {
295 return Parse(aValue.Value().GetAsString());
298 MOZ_ASSERT(aValue.Value().IsStringSequence());
300 const Sequence<nsString>& seq = aValue.Value().GetAsStringSequence();
301 if (seq.Length() == 0) {
302 return Err(NS_ERROR_FAILURE);
304 return Parse(seq);
307 void KeyPath::SetType(KeyPathType aType) {
308 mType = aType;
309 mStrings.Clear();
312 bool KeyPath::AppendStringWithValidation(const nsAString& aString) {
313 if (!IsValidKeyPathString(aString)) {
314 return false;
317 if (IsString()) {
318 NS_ASSERTION(mStrings.Length() == 0, "Too many strings!");
319 mStrings.AppendElement(aString);
320 return true;
323 if (IsArray()) {
324 mStrings.AppendElement(aString);
325 return true;
328 MOZ_ASSERT_UNREACHABLE("What?!");
329 return false;
332 nsresult KeyPath::ExtractKey(JSContext* aCx, const JS::Value& aValue,
333 Key& aKey) const {
334 uint32_t len = mStrings.Length();
335 JS::Rooted<JS::Value> value(aCx);
337 aKey.Unset();
339 for (uint32_t i = 0; i < len; ++i) {
340 nsresult rv =
341 GetJSValFromKeyPathString(aCx, aValue, mStrings[i], value.address(),
342 DoNotCreateProperties, nullptr, nullptr);
343 if (NS_FAILED(rv)) {
344 return rv;
347 auto result = aKey.AppendItem(aCx, IsArray() && i == 0, value);
348 if (result.isErr()) {
349 NS_ASSERTION(aKey.IsUnset(), "Encoding error should unset");
350 if (result.inspectErr().Is(SpecialValues::Exception)) {
351 result.unwrapErr().AsException().SuppressException();
353 return NS_ERROR_DOM_INDEXEDDB_DATA_ERR;
357 aKey.FinishArray();
359 return NS_OK;
362 nsresult KeyPath::ExtractKeyAsJSVal(JSContext* aCx, const JS::Value& aValue,
363 JS::Value* aOutVal) const {
364 NS_ASSERTION(IsValid(), "This doesn't make sense!");
366 if (IsString()) {
367 return GetJSValFromKeyPathString(aCx, aValue, mStrings[0], aOutVal,
368 DoNotCreateProperties, nullptr, nullptr);
371 const uint32_t len = mStrings.Length();
372 JS::Rooted<JSObject*> arrayObj(aCx, JS::NewArrayObject(aCx, len));
373 if (!arrayObj) {
374 return NS_ERROR_OUT_OF_MEMORY;
377 JS::Rooted<JS::Value> value(aCx);
378 for (uint32_t i = 0; i < len; ++i) {
379 nsresult rv =
380 GetJSValFromKeyPathString(aCx, aValue, mStrings[i], value.address(),
381 DoNotCreateProperties, nullptr, nullptr);
382 if (NS_FAILED(rv)) {
383 return rv;
386 if (!JS_DefineElement(aCx, arrayObj, i, value, JSPROP_ENUMERATE)) {
387 IDB_REPORT_INTERNAL_ERR();
388 return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
392 aOutVal->setObject(*arrayObj);
393 return NS_OK;
396 nsresult KeyPath::ExtractOrCreateKey(JSContext* aCx, const JS::Value& aValue,
397 Key& aKey,
398 ExtractOrCreateKeyCallback aCallback,
399 void* aClosure) const {
400 NS_ASSERTION(IsString(), "This doesn't make sense!");
402 JS::Rooted<JS::Value> value(aCx);
404 aKey.Unset();
406 nsresult rv =
407 GetJSValFromKeyPathString(aCx, aValue, mStrings[0], value.address(),
408 CreateProperties, aCallback, aClosure);
409 if (NS_FAILED(rv)) {
410 return rv;
413 auto result = aKey.AppendItem(aCx, false, value);
414 if (result.isErr()) {
415 NS_ASSERTION(aKey.IsUnset(), "Should be unset");
416 if (result.inspectErr().Is(SpecialValues::Exception)) {
417 result.unwrapErr().AsException().SuppressException();
419 return value.isUndefined() ? NS_OK : NS_ERROR_DOM_INDEXEDDB_DATA_ERR;
422 aKey.FinishArray();
424 return NS_OK;
427 nsAutoString KeyPath::SerializeToString() const {
428 NS_ASSERTION(IsValid(), "Check to see if I'm valid first!");
430 if (IsString()) {
431 return nsAutoString{mStrings[0]};
434 if (IsArray()) {
435 nsAutoString res;
437 // We use a comma in the beginning to indicate that it's an array of
438 // key paths. This is to be able to tell a string-keypath from an
439 // array-keypath which contains only one item.
440 // It also makes serializing easier :-)
441 const uint32_t len = mStrings.Length();
442 for (uint32_t i = 0; i < len; ++i) {
443 res.Append(',');
444 res.Append(mStrings[i]);
447 return res;
450 MOZ_ASSERT_UNREACHABLE("What?");
451 return {};
454 // static
455 KeyPath KeyPath::DeserializeFromString(const nsAString& aString) {
456 KeyPath keyPath(0);
458 if (!aString.IsEmpty() && aString.First() == ',') {
459 keyPath.SetType(KeyPathType::Array);
461 // We use a comma in the beginning to indicate that it's an array of
462 // key paths. This is to be able to tell a string-keypath from an
463 // array-keypath which contains only one item.
464 nsCharSeparatedTokenizerTemplate<NS_TokenizerIgnoreNothing> tokenizer(
465 aString, ',');
466 tokenizer.nextToken();
467 while (tokenizer.hasMoreTokens()) {
468 keyPath.mStrings.AppendElement(tokenizer.nextToken());
471 if (tokenizer.separatorAfterCurrentToken()) {
472 // There is a trailing comma, indicating the original KeyPath has
473 // a trailing empty string, i.e. [..., '']. We should append this
474 // empty string.
475 keyPath.mStrings.EmplaceBack();
478 return keyPath;
481 keyPath.SetType(KeyPathType::String);
482 keyPath.mStrings.AppendElement(aString);
484 return keyPath;
487 nsresult KeyPath::ToJSVal(JSContext* aCx,
488 JS::MutableHandle<JS::Value> aValue) const {
489 if (IsArray()) {
490 uint32_t len = mStrings.Length();
491 JS::Rooted<JSObject*> array(aCx, JS::NewArrayObject(aCx, len));
492 if (!array) {
493 IDB_WARNING("Failed to make array!");
494 return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
497 for (uint32_t i = 0; i < len; ++i) {
498 JS::Rooted<JS::Value> val(aCx);
499 nsString tmp(mStrings[i]);
500 if (!xpc::StringToJsval(aCx, tmp, &val)) {
501 IDB_REPORT_INTERNAL_ERR();
502 return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
505 if (!JS_DefineElement(aCx, array, i, val, JSPROP_ENUMERATE)) {
506 IDB_REPORT_INTERNAL_ERR();
507 return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
511 aValue.setObject(*array);
512 return NS_OK;
515 if (IsString()) {
516 nsString tmp(mStrings[0]);
517 if (!xpc::StringToJsval(aCx, tmp, aValue)) {
518 IDB_REPORT_INTERNAL_ERR();
519 return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
521 return NS_OK;
524 aValue.setNull();
525 return NS_OK;
528 nsresult KeyPath::ToJSVal(JSContext* aCx, JS::Heap<JS::Value>& aValue) const {
529 JS::Rooted<JS::Value> value(aCx);
530 nsresult rv = ToJSVal(aCx, &value);
531 if (NS_SUCCEEDED(rv)) {
532 aValue = value;
534 return rv;
537 bool KeyPath::IsAllowedForObjectStore(bool aAutoIncrement) const {
538 // Any keypath that passed validation is allowed for non-autoIncrement
539 // objectStores.
540 if (!aAutoIncrement) {
541 return true;
544 // Array keypaths are not allowed for autoIncrement objectStores.
545 if (IsArray()) {
546 return false;
549 // Neither are empty strings.
550 if (IsEmpty()) {
551 return false;
554 // Everything else is ok.
555 return true;
558 } // namespace mozilla::dom::indexedDB