tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

KeyPath.cpp (17578B)


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