Coverage Report

Created: 2018-09-25 14:53

/src/mozilla-central/dom/indexedDB/KeyPath.cpp
Line
Count
Source (jump to first uncovered line)
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/. */
6
7
#include "KeyPath.h"
8
#include "IDBObjectStore.h"
9
#include "Key.h"
10
#include "ReportInternalError.h"
11
12
#include "nsCharSeparatedTokenizer.h"
13
#include "nsJSUtils.h"
14
#include "nsPrintfCString.h"
15
#include "xpcpublic.h"
16
17
#include "mozilla/dom/BindingDeclarations.h"
18
#include "mozilla/dom/IDBObjectStoreBinding.h"
19
20
namespace mozilla {
21
namespace dom {
22
namespace indexedDB {
23
24
namespace {
25
26
inline
27
bool
28
IgnoreWhitespace(char16_t c)
29
0
{
30
0
  return false;
31
0
}
32
33
typedef nsCharSeparatedTokenizerTemplate<IgnoreWhitespace> KeyPathTokenizer;
34
35
bool
36
IsValidKeyPathString(const nsAString& aKeyPath)
37
0
{
38
0
  NS_ASSERTION(!aKeyPath.IsVoid(), "What?");
39
0
40
0
  KeyPathTokenizer tokenizer(aKeyPath, '.');
41
0
42
0
  while (tokenizer.hasMoreTokens()) {
43
0
    nsString token(tokenizer.nextToken());
44
0
45
0
    if (!token.Length()) {
46
0
      return false;
47
0
    }
48
0
49
0
    if (!JS_IsIdentifier(token.get(), token.Length())) {
50
0
      return false;
51
0
    }
52
0
  }
53
0
54
0
  // If the very last character was a '.', the tokenizer won't give us an empty
55
0
  // token, but the keyPath is still invalid.
56
0
  if (!aKeyPath.IsEmpty() &&
57
0
      aKeyPath.CharAt(aKeyPath.Length() - 1) == '.') {
58
0
    return false;
59
0
  }
60
0
61
0
  return true;
62
0
}
63
64
enum KeyExtractionOptions {
65
  DoNotCreateProperties,
66
  CreateProperties
67
};
68
69
nsresult
70
GetJSValFromKeyPathString(JSContext* aCx,
71
                          const JS::Value& aValue,
72
                          const nsAString& aKeyPathString,
73
                          JS::Value* aKeyJSVal,
74
                          KeyExtractionOptions aOptions,
75
                          KeyPath::ExtractOrCreateKeyCallback aCallback,
76
                          void* aClosure)
77
0
{
78
0
  NS_ASSERTION(aCx, "Null pointer!");
79
0
  NS_ASSERTION(IsValidKeyPathString(aKeyPathString),
80
0
               "This will explode!");
81
0
  NS_ASSERTION(!(aCallback || aClosure) || aOptions == CreateProperties,
82
0
               "This is not allowed!");
83
0
  NS_ASSERTION(aOptions != CreateProperties || aCallback,
84
0
               "If properties are created, there must be a callback!");
85
0
86
0
  nsresult rv = NS_OK;
87
0
  *aKeyJSVal = aValue;
88
0
89
0
  KeyPathTokenizer tokenizer(aKeyPathString, '.');
90
0
91
0
  nsString targetObjectPropName;
92
0
  JS::Rooted<JSObject*> targetObject(aCx, nullptr);
93
0
  JS::Rooted<JS::Value> currentVal(aCx, aValue);
94
0
  JS::Rooted<JSObject*> obj(aCx);
95
0
96
0
  while (tokenizer.hasMoreTokens()) {
97
0
    const nsDependentSubstring& token = tokenizer.nextToken();
98
0
99
0
    NS_ASSERTION(!token.IsEmpty(), "Should be a valid keypath");
100
0
101
0
    const char16_t* keyPathChars = token.BeginReading();
102
0
    const size_t keyPathLen = token.Length();
103
0
104
0
    bool hasProp;
105
0
    if (!targetObject) {
106
0
      // We're still walking the chain of existing objects
107
0
      // http://w3c.github.io/IndexedDB/#evaluate-a-key-path-on-a-value
108
0
      // step 4 substep 1: check for .length on a String value.
109
0
      if (currentVal.isString() && !tokenizer.hasMoreTokens() &&
110
0
          token.EqualsLiteral("length")) {
111
0
        aKeyJSVal->setNumber(double(JS_GetStringLength(currentVal.toString())));
112
0
        break;
113
0
      }
114
0
115
0
      if (!currentVal.isObject()) {
116
0
        return NS_ERROR_DOM_INDEXEDDB_DATA_ERR;
117
0
      }
118
0
      obj = &currentVal.toObject();
119
0
120
0
      bool ok = JS_HasUCProperty(aCx, obj, keyPathChars, keyPathLen,
121
0
                                 &hasProp);
122
0
      IDB_ENSURE_TRUE(ok, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
123
0
124
0
      if (hasProp) {
125
0
        // Get if the property exists...
126
0
        JS::Rooted<JS::Value> intermediate(aCx);
127
0
        bool ok = JS_GetUCProperty(aCx, obj, keyPathChars, keyPathLen, &intermediate);
128
0
        IDB_ENSURE_TRUE(ok, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
129
0
130
0
        // Treat explicitly undefined as an error.
131
0
        if (intermediate.isUndefined()) {
132
0
          return NS_ERROR_DOM_INDEXEDDB_DATA_ERR;
133
0
        }
134
0
        if (tokenizer.hasMoreTokens()) {
135
0
          // ...and walk to it if there are more steps...
136
0
          currentVal = intermediate;
137
0
        }
138
0
        else {
139
0
          // ...otherwise use it as key
140
0
          *aKeyJSVal = intermediate;
141
0
        }
142
0
      }
143
0
      else {
144
0
        // If the property doesn't exist, fall into below path of starting
145
0
        // to define properties, if allowed.
146
0
        if (aOptions == DoNotCreateProperties) {
147
0
          return NS_ERROR_DOM_INDEXEDDB_DATA_ERR;
148
0
        }
149
0
150
0
        targetObject = obj;
151
0
        targetObjectPropName = token;
152
0
      }
153
0
    }
154
0
155
0
    if (targetObject) {
156
0
      // We have started inserting new objects or are about to just insert
157
0
      // the first one.
158
0
159
0
      aKeyJSVal->setUndefined();
160
0
161
0
      if (tokenizer.hasMoreTokens()) {
162
0
        // If we're not at the end, we need to add a dummy object to the
163
0
        // chain.
164
0
        JS::Rooted<JSObject*> dummy(aCx, JS_NewPlainObject(aCx));
165
0
        if (!dummy) {
166
0
          IDB_REPORT_INTERNAL_ERR();
167
0
          rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
168
0
          break;
169
0
        }
170
0
171
0
        if (!JS_DefineUCProperty(aCx, obj, token.BeginReading(),
172
0
                                 token.Length(), dummy, JSPROP_ENUMERATE)) {
173
0
          IDB_REPORT_INTERNAL_ERR();
174
0
          rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
175
0
          break;
176
0
        }
177
0
178
0
        obj = dummy;
179
0
      }
180
0
      else {
181
0
        JS::Rooted<JSObject*> dummy(aCx,
182
0
          JS_NewObject(aCx, IDBObjectStore::DummyPropClass()));
183
0
        if (!dummy) {
184
0
          IDB_REPORT_INTERNAL_ERR();
185
0
          rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
186
0
          break;
187
0
        }
188
0
189
0
        if (!JS_DefineUCProperty(aCx, obj, token.BeginReading(),
190
0
                                 token.Length(), dummy, JSPROP_ENUMERATE)) {
191
0
          IDB_REPORT_INTERNAL_ERR();
192
0
          rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
193
0
          break;
194
0
        }
195
0
196
0
        obj = dummy;
197
0
      }
198
0
    }
199
0
  }
200
0
201
0
  // We guard on rv being a success because we need to run the property
202
0
  // deletion code below even if we should not be running the callback.
203
0
  if (NS_SUCCEEDED(rv) && aCallback) {
204
0
    rv = (*aCallback)(aCx, aClosure);
205
0
  }
206
0
207
0
  if (targetObject) {
208
0
    // If this fails, we lose, and the web page sees a magical property
209
0
    // appear on the object :-(
210
0
    JS::ObjectOpResult succeeded;
211
0
    if (!JS_DeleteUCProperty(aCx, targetObject,
212
0
                             targetObjectPropName.get(),
213
0
                             targetObjectPropName.Length(),
214
0
                             succeeded)) {
215
0
      IDB_REPORT_INTERNAL_ERR();
216
0
      return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
217
0
    }
218
0
    IDB_ENSURE_TRUE(succeeded, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
219
0
  }
220
0
221
0
  NS_ENSURE_SUCCESS(rv, rv);
222
0
  return rv;
223
0
}
224
225
} // namespace
226
227
// static
228
nsresult
229
KeyPath::Parse(const nsAString& aString, KeyPath* aKeyPath)
230
0
{
231
0
  KeyPath keyPath(0);
232
0
  keyPath.SetType(STRING);
233
0
234
0
  if (!keyPath.AppendStringWithValidation(aString)) {
235
0
    return NS_ERROR_FAILURE;
236
0
  }
237
0
238
0
  *aKeyPath = keyPath;
239
0
  return NS_OK;
240
0
}
241
242
//static
243
nsresult
244
KeyPath::Parse(const Sequence<nsString>& aStrings, KeyPath* aKeyPath)
245
0
{
246
0
  KeyPath keyPath(0);
247
0
  keyPath.SetType(ARRAY);
248
0
249
0
  for (uint32_t i = 0; i < aStrings.Length(); ++i) {
250
0
    if (!keyPath.AppendStringWithValidation(aStrings[i])) {
251
0
      return NS_ERROR_FAILURE;
252
0
    }
253
0
  }
254
0
255
0
  *aKeyPath = keyPath;
256
0
  return NS_OK;
257
0
}
258
259
// static
260
nsresult
261
KeyPath::Parse(const Nullable<OwningStringOrStringSequence>& aValue, KeyPath* aKeyPath)
262
0
{
263
0
  KeyPath keyPath(0);
264
0
265
0
  aKeyPath->SetType(NONEXISTENT);
266
0
267
0
  if (aValue.IsNull()) {
268
0
    *aKeyPath = keyPath;
269
0
    return NS_OK;
270
0
  }
271
0
272
0
  if (aValue.Value().IsString()) {
273
0
    return Parse(aValue.Value().GetAsString(), aKeyPath);
274
0
  }
275
0
276
0
  MOZ_ASSERT(aValue.Value().IsStringSequence());
277
0
278
0
  const Sequence<nsString>& seq = aValue.Value().GetAsStringSequence();
279
0
  if (seq.Length() == 0) {
280
0
    return NS_ERROR_FAILURE;
281
0
  }
282
0
  return Parse(seq, aKeyPath);
283
0
}
284
285
void
286
KeyPath::SetType(KeyPathType aType)
287
0
{
288
0
  mType = aType;
289
0
  mStrings.Clear();
290
0
}
291
292
bool
293
KeyPath::AppendStringWithValidation(const nsAString& aString)
294
0
{
295
0
  if (!IsValidKeyPathString(aString)) {
296
0
    return false;
297
0
  }
298
0
299
0
  if (IsString()) {
300
0
    NS_ASSERTION(mStrings.Length() == 0, "Too many strings!");
301
0
    mStrings.AppendElement(aString);
302
0
    return true;
303
0
  }
304
0
305
0
  if (IsArray()) {
306
0
    mStrings.AppendElement(aString);
307
0
    return true;
308
0
  }
309
0
310
0
  MOZ_ASSERT_UNREACHABLE("What?!");
311
0
  return false;
312
0
}
313
314
nsresult
315
KeyPath::ExtractKey(JSContext* aCx, const JS::Value& aValue, Key& aKey) const
316
0
{
317
0
  uint32_t len = mStrings.Length();
318
0
  JS::Rooted<JS::Value> value(aCx);
319
0
320
0
  aKey.Unset();
321
0
322
0
  for (uint32_t i = 0; i < len; ++i) {
323
0
    nsresult rv = GetJSValFromKeyPathString(aCx, aValue, mStrings[i],
324
0
                                            value.address(),
325
0
                                            DoNotCreateProperties, nullptr,
326
0
                                            nullptr);
327
0
    if (NS_FAILED(rv)) {
328
0
      return rv;
329
0
    }
330
0
331
0
    if (NS_FAILED(aKey.AppendItem(aCx, IsArray() && i == 0, value))) {
332
0
      NS_ASSERTION(aKey.IsUnset(), "Encoding error should unset");
333
0
      return NS_ERROR_DOM_INDEXEDDB_DATA_ERR;
334
0
    }
335
0
  }
336
0
337
0
  aKey.FinishArray();
338
0
339
0
  return NS_OK;
340
0
}
341
342
nsresult
343
KeyPath::ExtractKeyAsJSVal(JSContext* aCx, const JS::Value& aValue,
344
                           JS::Value* aOutVal) const
345
0
{
346
0
  NS_ASSERTION(IsValid(), "This doesn't make sense!");
347
0
348
0
  if (IsString()) {
349
0
    return GetJSValFromKeyPathString(aCx, aValue, mStrings[0], aOutVal,
350
0
                                     DoNotCreateProperties, nullptr, nullptr);
351
0
  }
352
0
353
0
  const uint32_t len = mStrings.Length();
354
0
  JS::Rooted<JSObject*> arrayObj(aCx, JS_NewArrayObject(aCx, len));
355
0
  if (!arrayObj) {
356
0
    return NS_ERROR_OUT_OF_MEMORY;
357
0
  }
358
0
359
0
  JS::Rooted<JS::Value> value(aCx);
360
0
  for (uint32_t i = 0; i < len; ++i) {
361
0
    nsresult rv = GetJSValFromKeyPathString(aCx, aValue, mStrings[i],
362
0
                                            value.address(),
363
0
                                            DoNotCreateProperties, nullptr,
364
0
                                            nullptr);
365
0
    if (NS_FAILED(rv)) {
366
0
      return rv;
367
0
    }
368
0
369
0
    if (!JS_DefineElement(aCx, arrayObj, i, value, JSPROP_ENUMERATE)) {
370
0
      IDB_REPORT_INTERNAL_ERR();
371
0
      return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
372
0
    }
373
0
  }
374
0
375
0
  aOutVal->setObject(*arrayObj);
376
0
  return NS_OK;
377
0
}
378
379
nsresult
380
KeyPath::ExtractOrCreateKey(JSContext* aCx, const JS::Value& aValue,
381
                            Key& aKey, ExtractOrCreateKeyCallback aCallback,
382
                            void* aClosure) const
383
0
{
384
0
  NS_ASSERTION(IsString(), "This doesn't make sense!");
385
0
386
0
  JS::Rooted<JS::Value> value(aCx);
387
0
388
0
  aKey.Unset();
389
0
390
0
  nsresult rv = GetJSValFromKeyPathString(aCx, aValue, mStrings[0],
391
0
                                          value.address(),
392
0
                                          CreateProperties, aCallback,
393
0
                                          aClosure);
394
0
  if (NS_FAILED(rv)) {
395
0
    return rv;
396
0
  }
397
0
398
0
  if (NS_FAILED(aKey.AppendItem(aCx, false, value))) {
399
0
    NS_ASSERTION(aKey.IsUnset(), "Should be unset");
400
0
    return value.isUndefined() ? NS_OK : NS_ERROR_DOM_INDEXEDDB_DATA_ERR;
401
0
  }
402
0
403
0
  aKey.FinishArray();
404
0
405
0
  return NS_OK;
406
0
}
407
408
void
409
KeyPath::SerializeToString(nsAString& aString) const
410
0
{
411
0
  NS_ASSERTION(IsValid(), "Check to see if I'm valid first!");
412
0
413
0
  if (IsString()) {
414
0
    aString = mStrings[0];
415
0
    return;
416
0
  }
417
0
418
0
  if (IsArray()) {
419
0
    // We use a comma in the beginning to indicate that it's an array of
420
0
    // key paths. This is to be able to tell a string-keypath from an
421
0
    // array-keypath which contains only one item.
422
0
    // It also makes serializing easier :-)
423
0
    uint32_t len = mStrings.Length();
424
0
    for (uint32_t i = 0; i < len; ++i) {
425
0
      aString.Append(',');
426
0
      aString.Append(mStrings[i]);
427
0
    }
428
0
429
0
    return;
430
0
  }
431
0
432
0
  MOZ_ASSERT_UNREACHABLE("What?");
433
0
}
434
435
// static
436
KeyPath
437
KeyPath::DeserializeFromString(const nsAString& aString)
438
0
{
439
0
  KeyPath keyPath(0);
440
0
441
0
  if (!aString.IsEmpty() && aString.First() == ',') {
442
0
    keyPath.SetType(ARRAY);
443
0
444
0
    // We use a comma in the beginning to indicate that it's an array of
445
0
    // key paths. This is to be able to tell a string-keypath from an
446
0
    // array-keypath which contains only one item.
447
0
    nsCharSeparatedTokenizerTemplate<IgnoreWhitespace> tokenizer(aString, ',');
448
0
    tokenizer.nextToken();
449
0
    while (tokenizer.hasMoreTokens()) {
450
0
      keyPath.mStrings.AppendElement(tokenizer.nextToken());
451
0
    }
452
0
453
0
    return keyPath;
454
0
  }
455
0
456
0
  keyPath.SetType(STRING);
457
0
  keyPath.mStrings.AppendElement(aString);
458
0
459
0
  return keyPath;
460
0
}
461
462
nsresult
463
KeyPath::ToJSVal(JSContext* aCx, JS::MutableHandle<JS::Value> aValue) const
464
0
{
465
0
  if (IsArray()) {
466
0
    uint32_t len = mStrings.Length();
467
0
    JS::Rooted<JSObject*> array(aCx, JS_NewArrayObject(aCx, len));
468
0
    if (!array) {
469
0
      IDB_WARNING("Failed to make array!");
470
0
      return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
471
0
    }
472
0
473
0
    for (uint32_t i = 0; i < len; ++i) {
474
0
      JS::Rooted<JS::Value> val(aCx);
475
0
      nsString tmp(mStrings[i]);
476
0
      if (!xpc::StringToJsval(aCx, tmp, &val)) {
477
0
        IDB_REPORT_INTERNAL_ERR();
478
0
        return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
479
0
      }
480
0
481
0
      if (!JS_DefineElement(aCx, array, i, val, JSPROP_ENUMERATE)) {
482
0
        IDB_REPORT_INTERNAL_ERR();
483
0
        return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
484
0
      }
485
0
    }
486
0
487
0
    aValue.setObject(*array);
488
0
    return NS_OK;
489
0
  }
490
0
491
0
  if (IsString()) {
492
0
    nsString tmp(mStrings[0]);
493
0
    if (!xpc::StringToJsval(aCx, tmp, aValue)) {
494
0
      IDB_REPORT_INTERNAL_ERR();
495
0
      return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
496
0
    }
497
0
    return NS_OK;
498
0
  }
499
0
500
0
  aValue.setNull();
501
0
  return NS_OK;
502
0
}
503
504
nsresult
505
KeyPath::ToJSVal(JSContext* aCx, JS::Heap<JS::Value>& aValue) const
506
0
{
507
0
  JS::Rooted<JS::Value> value(aCx);
508
0
  nsresult rv = ToJSVal(aCx, &value);
509
0
  if (NS_SUCCEEDED(rv)) {
510
0
    aValue = value;
511
0
  }
512
0
  return rv;
513
0
}
514
515
bool
516
KeyPath::IsAllowedForObjectStore(bool aAutoIncrement) const
517
0
{
518
0
  // Any keypath that passed validation is allowed for non-autoIncrement
519
0
  // objectStores.
520
0
  if (!aAutoIncrement) {
521
0
    return true;
522
0
  }
523
0
524
0
  // Array keypaths are not allowed for autoIncrement objectStores.
525
0
  if (IsArray()) {
526
0
    return false;
527
0
  }
528
0
529
0
  // Neither are empty strings.
530
0
  if (IsEmpty()) {
531
0
    return false;
532
0
  }
533
0
534
0
  // Everything else is ok.
535
0
  return true;
536
0
}
537
538
} // namespace indexedDB
539
} // namespace dom
540
} // namespace mozilla