lib/VM/JSLib/RuntimeJSONUtils.cpp (849 lines of code) (raw):

/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #include "hermes/VM/JSLib/RuntimeJSONUtils.h" #include "Object.h" #include "hermes/Support/Compiler.h" #include "hermes/Support/JSON.h" #include "hermes/Support/UTF16Stream.h" #include "hermes/VM/ArrayLike.h" #include "hermes/VM/ArrayStorage.h" #include "hermes/VM/Callable.h" #include "hermes/VM/JSArray.h" #include "hermes/VM/JSProxy.h" #include "hermes/VM/PrimitiveBox.h" #include "JSONLexer.h" #include "llvh/ADT/SmallString.h" #include "llvh/Support/SaveAndRestore.h" namespace hermes { namespace vm { namespace { /// This class wraps the functionality required to parse a JSON string into /// a VM runtime value. It expects a UTF8 string as input, and returns a /// HermesValue when parse is called. class RuntimeJSONParser { public: static constexpr int32_t MAX_RECURSION_DEPTH = #ifndef HERMES_LIMIT_STACK_DEPTH 512 #else 100 #endif ; private: /// The VM runtime. Runtime &runtime_; /// The lexer. JSONLexer lexer_; /// Stores the optional reviver parameter. /// https://es5.github.io/#x15.12.2 Handle<Callable> reviver_; /// A temporary handle, to avoid creating new handles when a temporary one /// is needed to protect some HermesValue. MutableHandle<> tmpHandle_; /// How many more nesting levels we allow before error. /// Decremented every time a nested level is started, /// and incremented again when leaving the nest. /// If it drops below 0 while parsing, raise a stack overflow. int32_t remainingDepth_{MAX_RECURSION_DEPTH}; public: explicit RuntimeJSONParser( Runtime &runtime, UTF16Stream &&jsonString, Handle<Callable> reviver) : runtime_(runtime), lexer_(runtime, std::move(jsonString)), reviver_(reviver), tmpHandle_(runtime) {} /// Parse JSON string through lexer_, create objects using runtime_. /// If errors occur, this function will return undefined, and the error /// should be kept inside SourceErrorManager. CallResult<HermesValue> parse(); private: /// Parse a JSON value, starting from the current token. /// When this function is finished, the current token will be set /// to the next token after the current parsed value. CallResult<HermesValue> parseValue(); /// Parse a JSON array, starting from the "[" token. /// When this function is finished, the current token must be "]". CallResult<HermesValue> parseArray(); /// Parse a JSON object, starting from the "{" token. /// When this function is finished, the current token must be "}". CallResult<HermesValue> parseObject(); /// Use reviver to filter the result. CallResult<HermesValue> revive(Handle<> value); /// The abstract operation Walk is a recursive abstract operation that takes /// two parameters: a holder object and the String name of a property in CallResult<HermesValue> operationWalk( Handle<JSObject> holder, Handle<> property); /// Given value and key, recursively call operationWalk to generate a new /// filtered value. It's a helper function for operationWalk, and does not /// need to return a value ExecutionStatus filter(Handle<JSObject> val, Handle<> key); }; /// This class wraps the functionality required to stringify an object /// as JSON. class JSONStringifyer { /// The runtime. Runtime &runtime_; /// The ReplacerFunction, initialized from the "replacer" argument in /// stringify. MutableHandle<Callable> replacerFunction_; /// The gap string, which will be used to construct indent. /// Initialized from the "space" argument in stringify. /// gap_ will be nullptr if no gap is specified or if the gap is an empty /// string. This means that if gap_ is not nullptr, it will not be /// an empty string. MutableHandle<StringPrimitive> gap_; /// The PropertyList, constructed from the "replacer" argument in stringify. MutableHandle<JSArray> propertyList_; /// The stack, used at runtime by operationJA and operationJO to store /// `value_` for recursions. MutableHandle<PropStorage> stackValue_; /// An additional stack just for operationJO to store `K` for recursions. MutableHandle<PropStorage> stackJO_; /// A temporary handle, to avoid creating new handles when a temporary one /// is needed. /// Note: this should be used with care because it can be shared among /// functions. MutableHandle<> tmpHandle_; /// A second temporary handle for when the first is taken. MutableHandle<> tmpHandle2_; /// Handle used by operationStr to store the value. MutableHandle<> operationStrValue_; /// Handle used by operationJO to store K. MutableHandle<JSArray> operationJOK_; /// The holder argument passed to operationStr. /// We define a member variable here to avoid creating a new handle /// each time we are calling operationStr. MutableHandle<JSObject> operationStrHolder_; /// The current depth of recursion, used at runtime by operationJA and /// operationJO. This is used as a stack overflow guard in stringifying. It /// also doubles as an indent counter for prettified JS. uint32_t depthCount_{0}; /// The max amount that depthCount_ is allowed to reach. Once it's reached, an /// exception will be thrown. static constexpr uint32_t MAX_RECURSION_DEPTH{ RuntimeJSONParser::MAX_RECURSION_DEPTH}; /// The output buffer. The serialization process will append into it. llvh::SmallVector<char16_t, 32> output_{}; public: explicit JSONStringifyer(Runtime &runtime) : runtime_(runtime), replacerFunction_(runtime), gap_{runtime, nullptr}, propertyList_(runtime), stackValue_(runtime), stackJO_(runtime), tmpHandle_(runtime), tmpHandle2_(runtime), operationStrValue_(runtime), operationJOK_(runtime), operationStrHolder_(runtime) {} LLVM_NODISCARD ExecutionStatus init(Handle<> replacer, Handle<> space) { auto arrRes = PropStorage::create(runtime_, 4); if (LLVM_UNLIKELY(arrRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } stackValue_ = vmcast<PropStorage>(*arrRes); if (LLVM_UNLIKELY( (arrRes = PropStorage::create(runtime_, 4)) == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } stackJO_ = vmcast<PropStorage>(*arrRes); auto cr = initializeReplacer(replacer); if (LLVM_UNLIKELY(cr == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } return initializeSpace(space); } /// Stringify \p value. CallResult<HermesValue> stringify(Handle<> value); private: /// Check the type of replacer, initialize /// ReplacerFunction (replacerFunction_) and PropertyList (propertyList_). /// Covers step 3 and 4 in ES5.1 15.12.3. LLVM_NODISCARD ExecutionStatus initializeReplacer(Handle<> replacer); /// Check the type of space, initialize gap (gap_). /// Covers step 5, 6, 7, 8 in ES5.1 15.12.3. ExecutionStatus initializeSpace(Handle<> space); /// Implement the Str(key, holder) abstract operation to serialize a value. /// According to the spec, \p key should always be a string. /// However if this function is called from operationJA, we /// we don't want to convert every index into string. /// Hence we leave the key as it is, and convert them to string if needed. /// The holder is always stored in operationStrHolder_ by caller. /// \return whether the result is not undefined. CallResult<bool> operationStr(HermesValue key); /// Implement the abstract operation Quote(value). /// It wraps a String value in double quotes and escapes characters within it. void operationQuote(StringView value); /// Implement the abstract operation JA(value). The value to operate on /// is always the current last element in stackValue_. /// It serializes an array. ExecutionStatus operationJA(); /// Implement the abstract operation JO(value). The value to operate on /// is always the current last element in stackValue_. /// It serializes an object. ExecutionStatus operationJO(); /// Append '\n' and indent to output_. /// The indent is constructed according to depthCount_. void indent(); /// Push a value to stack when traversing the object recursively. /// It also checks if the value already exsists in the stack for /// cyclic recursion. /// \return true if the push is successful (no cyclic). CallResult<bool> pushValueToStack(HermesValue value); /// Pop the top of stack. void popValueFromStack(); /// Append the string indicated as \p identifierID to output_. void appendToOutput(SymbolID identifierID); /// Append the string indicated as \p str to output_. void appendToOutput(const StringPrimitive *str); }; } // namespace CallResult<HermesValue> RuntimeJSONParser::parse() { // parseValue() requires one token to start with. if (LLVM_UNLIKELY(lexer_.advance() == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } auto parRes = parseValue(); if (LLVM_UNLIKELY(parRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } // Make sure the next token must be EOF. if (LLVM_UNLIKELY(lexer_.getCurToken()->getKind() != JSONTokenKind::Eof)) { return lexer_.errorWithChar( "Unexpected token: ", lexer_.getCurToken()->getFirstChar()); } if (reviver_.get()) { if ((parRes = revive(runtime_.makeHandle(*parRes))) == ExecutionStatus::EXCEPTION) return ExecutionStatus::EXCEPTION; } return parRes; } CallResult<HermesValue> RuntimeJSONParser::parseValue() { llvh::SaveAndRestore<decltype(remainingDepth_)> oldDepth{ remainingDepth_, remainingDepth_ - 1}; if (remainingDepth_ <= 0) { return runtime_.raiseStackOverflow(Runtime::StackOverflowKind::JSONParser); } MutableHandle<> returnValue{runtime_}; switch (lexer_.getCurToken()->getKind()) { case JSONTokenKind::String: returnValue = lexer_.getCurToken()->getString().getHermesValue(); break; case JSONTokenKind::Number: returnValue = HermesValue::encodeDoubleValue(lexer_.getCurToken()->getNumber()); break; case JSONTokenKind::LBrace: { auto parRes = parseObject(); if (LLVM_UNLIKELY(parRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } returnValue = *parRes; break; } case JSONTokenKind::LSquare: { auto parRes = parseArray(); if (LLVM_UNLIKELY(parRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } returnValue = *parRes; break; } case JSONTokenKind::True: returnValue = HermesValue::encodeBoolValue(true); break; case JSONTokenKind::False: returnValue = HermesValue::encodeBoolValue(false); break; case JSONTokenKind::Null: returnValue = HermesValue::encodeNullValue(); break; default: if (lexer_.getCurToken()->getKind() == JSONTokenKind::Eof) { return lexer_.error("Unexpected end of input"); } return lexer_.errorWithChar( "Unexpected token: ", lexer_.getCurToken()->getFirstChar()); } if (LLVM_UNLIKELY(lexer_.advance() == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } return returnValue.getHermesValue(); } CallResult<HermesValue> RuntimeJSONParser::parseArray() { assert( lexer_.getCurToken()->getKind() == JSONTokenKind::LSquare && "Wrong entrance to parseArray"); auto arrRes = JSArray::create(runtime_, 4, 0); if (LLVM_UNLIKELY(arrRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } auto array = *arrRes; if (LLVM_UNLIKELY(lexer_.advance() == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } if (lexer_.getCurToken()->getKind() != JSONTokenKind::RSquare) { MutableHandle<> indexValue{runtime_}; GCScope gcScope{runtime_}; auto marker = gcScope.createMarker(); for (uint32_t index = 0;; ++index) { gcScope.flushToMarker(marker); auto parRes = parseValue(); if (LLVM_UNLIKELY(parRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } indexValue = HermesValue::encodeDoubleValue(index); (void)JSObject::defineOwnComputedPrimitive( array, runtime_, indexValue, DefinePropertyFlags::getDefaultNewPropertyFlags(), runtime_.makeHandle(*parRes)); if (lexer_.getCurToken()->getKind() == JSONTokenKind::Comma) { if (LLVM_UNLIKELY(lexer_.advance() == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } continue; } else if (lexer_.getCurToken()->getKind() == JSONTokenKind::RSquare) { break; } else { return lexer_.error("Expect ']'"); } } assert( lexer_.getCurToken()->getKind() == JSONTokenKind::RSquare && "Unexpected break for array parse"); } return array.getHermesValue(); } CallResult<HermesValue> RuntimeJSONParser::parseObject() { assert( lexer_.getCurToken()->getKind() == JSONTokenKind::LBrace && "Wrong entrance to parseObject"); auto object = runtime_.makeHandle(JSObject::create(runtime_)); if (LLVM_UNLIKELY(lexer_.advance() == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } if (lexer_.getCurToken()->getKind() != JSONTokenKind::RBrace) { MutableHandle<StringPrimitive> key{runtime_}; GCScope gcScope{runtime_}; auto marker = gcScope.createMarker(); for (;;) { gcScope.flushToMarker(marker); if (LLVM_UNLIKELY( lexer_.getCurToken()->getKind() != JSONTokenKind::String)) { return lexer_.error("Expect a string key in JSON object"); } key = lexer_.getCurToken()->getString().get(); if (LLVM_UNLIKELY(lexer_.advance() == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } if (lexer_.getCurToken()->getKind() != JSONTokenKind::Colon) { return lexer_.error("Expect ':' after the key in JSON object"); } if (LLVM_UNLIKELY(lexer_.advance() == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } auto parRes = parseValue(); if (LLVM_UNLIKELY(parRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } (void)JSObject::defineOwnComputedPrimitive( object, runtime_, key, DefinePropertyFlags::getDefaultNewPropertyFlags(), runtime_.makeHandle(*parRes)); if (lexer_.getCurToken()->getKind() == JSONTokenKind::Comma) { if (LLVM_UNLIKELY(lexer_.advance() == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } continue; } else if (lexer_.getCurToken()->getKind() == JSONTokenKind::RBrace) { break; } else { return lexer_.error("Expect '}'"); } } assert( lexer_.getCurToken()->getKind() == JSONTokenKind::RBrace && "Unexpected stop for object parse"); } return object.getHermesValue(); } CallResult<HermesValue> RuntimeJSONParser::revive(Handle<> value) { auto root = runtime_.makeHandle(JSObject::create(runtime_)); auto status = JSObject::defineOwnProperty( root, runtime_, Predefined::getSymbolID(Predefined::emptyString), DefinePropertyFlags::getDefaultNewPropertyFlags(), value); (void)status; assert( status != ExecutionStatus::EXCEPTION && *status && "defineOwnProperty on new object cannot fail"); return operationWalk( root, runtime_.getPredefinedStringHandle(Predefined::emptyString)); } CallResult<HermesValue> RuntimeJSONParser::operationWalk( Handle<JSObject> holder, Handle<> property) { // The operation is recursive so it needs a GCScope. GCScope gcScope(runtime_); llvh::SaveAndRestore<decltype(remainingDepth_)> oldDepth{ remainingDepth_, remainingDepth_ - 1}; if (remainingDepth_ <= 0) { return runtime_.raiseStackOverflow(Runtime::StackOverflowKind::JSONParser); } auto propRes = JSObject::getComputed_RJS(holder, runtime_, property); if (LLVM_UNLIKELY(propRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } MutableHandle<> tmpHandle{runtime_}; CallResult<bool> isArrayRes = isArray(runtime_, dyn_vmcast<JSObject>(propRes->get())); if (LLVM_UNLIKELY(isArrayRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } auto valHandle = runtime_.makeHandle(std::move(*propRes)); if (*isArrayRes) { Handle<JSObject> objHandle = Handle<JSObject>::vmcast(valHandle); CallResult<uint64_t> lenRes = getArrayLikeLength(objHandle, runtime_); if (LLVM_UNLIKELY(lenRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } GCScopeMarkerRAII marker(runtime_); for (uint64_t index = 0, e = *lenRes; index < e; ++index) { tmpHandle = HermesValue::encodeDoubleValue(index); // Note that deleting elements doesn't affect array length. if (LLVM_UNLIKELY( filter(objHandle, tmpHandle) == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } marker.flush(); } } else if (auto scopedObject = Handle<JSObject>::dyn_vmcast(valHandle)) { auto cr = JSObject::getOwnPropertyNames(scopedObject, runtime_, true); if (cr == ExecutionStatus::EXCEPTION) { return ExecutionStatus::EXCEPTION; } auto keys = *cr; GCScopeMarkerRAII marker(runtime_); for (uint32_t index = 0, e = keys->getEndIndex(); index < e; ++index) { tmpHandle = keys->at(runtime_, index); if (LLVM_UNLIKELY( filter(scopedObject, tmpHandle) == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } marker.flush(); } } // We have delayed converting the property to a string if it was index. // Now we have to do it because we are passing it to the reviver. tmpHandle = *property; auto strRes = toString_RJS(runtime_, tmpHandle); if (LLVM_UNLIKELY(strRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } tmpHandle = strRes->getHermesValue(); return Callable::executeCall2( reviver_, runtime_, holder, *tmpHandle, *valHandle) .toCallResultHermesValue(); } ExecutionStatus RuntimeJSONParser::filter(Handle<JSObject> val, Handle<> key) { auto jsonRes = operationWalk(val, key); if (LLVM_UNLIKELY(jsonRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } auto newElement = runtime_.makeHandle(*jsonRes); if (newElement->isUndefined()) { if (LLVM_UNLIKELY( JSObject::deleteComputed(val, runtime_, key) == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } } else { if (LLVM_UNLIKELY( JSObject::defineOwnComputed( val, runtime_, key, DefinePropertyFlags::getDefaultNewPropertyFlags(), newElement) == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } } return ExecutionStatus::RETURNED; } CallResult<HermesValue> runtimeJSONParse( Runtime &runtime, Handle<StringPrimitive> jsonString, Handle<Callable> reviver) { // Our parser requires UTF16 data that does not move during GCs, so // in most cases we'll need to copy, except for external 16-bit strings. UTF16Ref ref; SmallU16String<32> storage; if (LLVM_UNLIKELY(jsonString->isExternal() && !jsonString->isASCII())) { ref = jsonString->getStringRef<char16_t>(); } else { StringPrimitive::createStringView(runtime, jsonString) .appendUTF16String(storage); ref = storage; } RuntimeJSONParser parser{runtime, UTF16Stream(ref), reviver}; return parser.parse(); } CallResult<HermesValue> runtimeJSONParseRef( Runtime &runtime, UTF16Stream &&stream) { RuntimeJSONParser parser{ runtime, std::move(stream), Runtime::makeNullHandle<Callable>()}; return parser.parse(); } ExecutionStatus JSONStringifyer::initializeReplacer(Handle<> replacer) { if (!vmisa<JSObject>(*replacer)) return ExecutionStatus::RETURNED; // replacer is an object. if ((replacerFunction_ = dyn_vmcast<Callable>(*replacer))) return ExecutionStatus::RETURNED; // replacer is not a callable. auto replacerArray = Handle<JSObject>::dyn_vmcast(replacer); CallResult<bool> isArrayRes = isArray(runtime_, dyn_vmcast<JSObject>(*replacerArray)); if (LLVM_UNLIKELY(isArrayRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } if (!*isArrayRes) return ExecutionStatus::RETURNED; // replacer is arrayish CallResult<uint64_t> lenRes = getArrayLikeLength(replacerArray, runtime_); if (LLVM_UNLIKELY(lenRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } if (*lenRes > UINT32_MAX) { return runtime_.raiseRangeError("replacer array is too large"); } uint32_t len = static_cast<uint32_t>(*lenRes); auto arrRes = JSArray::create(runtime_, len, 0); if (LLVM_UNLIKELY(arrRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } propertyList_ = arrRes->get(); // Iterate through all indexes, in ascending order. GCScope gcScope{runtime_}; auto marker = gcScope.createMarker(); for (uint64_t i = 0, e = *lenRes; i < e; ++i) { gcScope.flushToMarker(marker); // Get the property value. tmpHandle_ = HermesValue::encodeDoubleValue(i); auto propRes = JSObject::getComputed_RJS(replacerArray, runtime_, tmpHandle_); if (LLVM_UNLIKELY(propRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } PseudoHandle<> v = std::move(*propRes); // Convert v to string and store into item, if v is string, number, JSString // or JSNumber. if (v->isString()) { tmpHandle_ = std::move(v); } else if ( v->isNumber() || vmisa<JSNumber>(v.get()) || vmisa<JSString>(v.get())) { tmpHandle_ = std::move(v); auto strRes = toString_RJS(runtime_, tmpHandle_); if (LLVM_UNLIKELY(strRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } tmpHandle_ = strRes->getHermesValue(); } else { tmpHandle_ = HermesValue::encodeUndefinedValue(); } if (tmpHandle_->isUndefined()) continue; // We only add item to propertyList if item is not already an element. bool exists = false; auto len = propertyList_->getEndIndex(); for (uint32_t i = 0; i < len; ++i) { if (propertyList_->at(runtime_, i) .getString() ->equals(tmpHandle_->getString())) { exists = true; break; } } if (!exists) { JSArray::setElementAt(propertyList_, runtime_, len, tmpHandle_); } } return ExecutionStatus::RETURNED; } ExecutionStatus JSONStringifyer::initializeSpace(Handle<> space) { tmpHandle_ = *space; if (vmisa<JSNumber>(*tmpHandle_)) { auto numRes = toNumber_RJS(runtime_, tmpHandle_); if (LLVM_UNLIKELY(numRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } tmpHandle_ = *numRes; } else if (vmisa<JSString>(*tmpHandle_)) { auto strRes = toString_RJS(runtime_, tmpHandle_); if (LLVM_UNLIKELY(strRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } tmpHandle_ = strRes->getHermesValue(); } if (tmpHandle_->isNumber()) { auto intRes = toIntegerOrInfinity(runtime_, tmpHandle_); assert( intRes != ExecutionStatus::EXCEPTION && "toInteger on a number cannot throw"); // Clamp result to [0,10]. auto spaceCount = static_cast<int>(std::max(0.0, std::min(10.0, intRes->getNumber()))); if (spaceCount > 0) { // Construct a string with spaceCount spaces. llvh::SmallString<32> spaces; for (int i = 0; i < spaceCount; ++i) { spaces.push_back(' '); } auto strRes = StringPrimitive::create(runtime_, spaces); if (LLVM_UNLIKELY(strRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } gap_ = strRes->getString(); } } else if (auto str = Handle<StringPrimitive>::dyn_vmcast(tmpHandle_)) { if (str->getStringLength() > 10) { auto strRes = StringPrimitive::slice(runtime_, str, 0, 10); if (LLVM_UNLIKELY(strRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } gap_ = strRes->getString(); } else if (str->getStringLength() > 0) { // If the string is empty, we don't set the gap. gap_ = str.get(); } } return ExecutionStatus::RETURNED; } CallResult<bool> JSONStringifyer::operationStr(HermesValue key) { GCScopeMarkerRAII marker{runtime_}; tmpHandle_ = key; // Str.1: access holder[key]. auto propRes = JSObject::getComputed_RJS(operationStrHolder_, runtime_, tmpHandle_); if (LLVM_UNLIKELY(propRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } operationStrValue_.set(propRes->get()); if (auto valueObj = Handle<JSObject>::dyn_vmcast(operationStrValue_)) { // Str.2. // Str.2.a: check if toJSON exists in value. if (LLVM_UNLIKELY( (propRes = JSObject::getNamed_RJS( valueObj, runtime_, Predefined::getSymbolID(Predefined::toJSON))) == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } // Str.2.b: check if toJSON is a Callable. if (auto toJSON = Handle<Callable>::dyn_vmcast( runtime_.makeHandle(std::move(*propRes)))) { if (!tmpHandle_->isString()) { // Lazily convert key to a string. auto status = toString_RJS(runtime_, tmpHandle_); assert( status != ExecutionStatus::EXCEPTION && "toString on a property cannot fail"); tmpHandle_ = status->getHermesValue(); } // Call toJSON with key as argument, value as this. auto callRes = Callable::executeCall1( toJSON, runtime_, operationStrValue_, *tmpHandle_); if (LLVM_UNLIKELY(callRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } operationStrValue_ = std::move(*callRes); } } // Str.3. if (replacerFunction_) { // Str.3.a. if (!tmpHandle_->isString()) { // Lazily convert key to a string. auto status = toString_RJS(runtime_, tmpHandle_); assert( status != ExecutionStatus::EXCEPTION && "toString on a property cannot fail"); tmpHandle_ = status->getHermesValue(); } // If ReplacerFunction exists, call it with key and value as argument, // holder as this. auto callRes = Callable::executeCall2( replacerFunction_, runtime_, operationStrHolder_, *tmpHandle_, *operationStrValue_); if (LLVM_UNLIKELY(callRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } operationStrValue_ = std::move(*callRes); } // Str.4: unbox value if necessary. // If Type(value) is Object, then if (vmisa<JSNumber>(*operationStrValue_)) { // If value has a [[NumberData]] internal slot, then // Set value to ? ToNumber(value). auto numRes = toNumber_RJS(runtime_, operationStrValue_); if (LLVM_UNLIKELY(numRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } operationStrValue_ = *numRes; } else if (vmisa<JSString>(*operationStrValue_)) { // Else if value has a [[StringData]] internal slot, then // Set value to ? ToString(value). auto strRes = toString_RJS(runtime_, operationStrValue_); if (LLVM_UNLIKELY(strRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } operationStrValue_ = strRes->getHermesValue(); } else if (auto *jsBool = dyn_vmcast<JSBoolean>(*operationStrValue_)) { // Else if value has a [[BooleanData]] internal slot, then // Set value to value.[[BooleanData]]. operationStrValue_ = HermesValue::encodeBoolValue(jsBool->getPrimitiveBoolean()); } // Str.5. if (operationStrValue_->isNull()) { appendToOutput(Predefined::getSymbolID(Predefined::null)); return true; } if (operationStrValue_->isBool()) { if (operationStrValue_->getBool()) { // Str.6. appendToOutput(Predefined::getSymbolID(Predefined::trueStr)); } else { // Str.7. appendToOutput(Predefined::getSymbolID(Predefined::falseStr)); } return true; } // Str.8. if (operationStrValue_->isString()) { operationQuote(StringPrimitive::createStringView( runtime_, Handle<StringPrimitive>::vmcast(operationStrValue_))); return true; } // Str.9. if (operationStrValue_->isNumber()) { if (std::isfinite(operationStrValue_->getNumber())) { auto status = toString_RJS(runtime_, operationStrValue_); assert( status != ExecutionStatus::EXCEPTION && "toString on a number cannot fail"); appendToOutput(status->get()); } else { appendToOutput(Predefined::getSymbolID(Predefined::null)); } return true; } // Str.10. if (vmisa<JSObject>(*operationStrValue_) && !vmisa<Callable>(*operationStrValue_)) { auto cr = pushValueToStack(*operationStrValue_); if (cr == ExecutionStatus::EXCEPTION) { return ExecutionStatus::EXCEPTION; } if (LLVM_UNLIKELY(!*cr)) { return runtime_.raiseTypeError("cyclical structure in JSON object"); } // Flush just before the recursive call (pushValueToStack can create // handles). marker.flush(); CallResult<bool> isArrayRes = isArray(runtime_, vmcast<JSObject>(*operationStrValue_)); if (LLVM_UNLIKELY(isArrayRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } ExecutionStatus status = *isArrayRes ? operationJA() : operationJO(); popValueFromStack(); if (LLVM_UNLIKELY(status == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } return true; } // Str.11. return false; } void JSONStringifyer::operationQuote(StringView value) { quoteStringForJSON(output_, value); } ExecutionStatus JSONStringifyer::operationJA() { GCScopeMarkerRAII marker{runtime_}; // JA.3. auto stepBack = depthCount_; // JA.4. if (depthCount_ + 1 >= MAX_RECURSION_DEPTH) { return runtime_.raiseStackOverflow( Runtime::StackOverflowKind::JSONStringify); } depthCount_++; output_.push_back(u'['); CallResult<uint64_t> lenRes = getArrayLikeLength( runtime_.makeHandle(vmcast<JSObject>( stackValue_->at(stackValue_->size() - 1).getObject(runtime_))), runtime_); if (LLVM_UNLIKELY(lenRes == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } if (*lenRes > 0) { // If array is not empty, we need to lead with an indent. indent(); } // JA.5, 6, 7, 8. for (uint64_t index = 0; index < *lenRes; ++index) { if (index > 0) { // JA.10. output_.push_back(u','); indent(); } // JA.8.a. operationStrHolder_ = vmcast<JSObject>( stackValue_->at(stackValue_->size() - 1).getObject(runtime_)); // Flush just before the recursion in case any handles were created. marker.flush(); auto status = operationStr(HermesValue::encodeDoubleValue(index)); if (LLVM_UNLIKELY(status == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } if (LLVM_UNLIKELY(!status.getValue())) { // operationStr returns undefined, we need to replace with null. appendToOutput(Predefined::getSymbolID(Predefined::null)); } } depthCount_ = stepBack; if (*lenRes > 0) { indent(); } output_.push_back(u']'); return ExecutionStatus::RETURNED; } ExecutionStatus JSONStringifyer::operationJO() { GCScopeMarkerRAII marker{runtime_}; // JO.3. auto stepBack = depthCount_; // JO.4. if (depthCount_ + 1 >= MAX_RECURSION_DEPTH) { return runtime_.raiseStackOverflow( Runtime::StackOverflowKind::JSONStringify); } depthCount_++; output_.push_back(u'{'); auto beginningLoc = output_.size(); indent(); if (propertyList_) { // JO.5. operationJOK_ = propertyList_.get(); } else { // JO.6. tmpHandle_ = HermesValue::encodeObjectValue( stackValue_->at(stackValue_->size() - 1).getObject(runtime_)); if (LLVM_LIKELY(!Handle<JSObject>::vmcast(tmpHandle_)->isProxyObject())) { // enumerableOwnProperties_RJS is the spec definition, and is // used below on proxies so the correct traps get called. In // the common case of a non-proxy object, we can do less work by // calling getOwnPropertyNames. auto cr = JSObject::getOwnPropertyNames( Handle<JSObject>::vmcast(tmpHandle_), runtime_, true); if (cr == ExecutionStatus::EXCEPTION) { return ExecutionStatus::EXCEPTION; } operationJOK_ = **cr; } else { CallResult<HermesValue> ownPropRes = enumerableOwnProperties_RJS( runtime_, Handle<JSObject>::vmcast(tmpHandle_), EnumerableOwnPropertiesKind::Key); if (ownPropRes == ExecutionStatus::EXCEPTION) { return ExecutionStatus::EXCEPTION; } operationJOK_ = vmcast<JSArray>(*ownPropRes); } } marker.flush(); // JO.8. bool hasElement = false; for (uint32_t index = 0, len = operationJOK_->getEndIndex(); index < len; ++index) { // JO.8.a. // We are speculating that the Str operation will not return undefined, // and just append the key/value pair to the output. If it turns out // that the Str operation does return undefined, we roll back to // curLocation. auto savedLocation = output_.size(); if (hasElement) { // JO.10. output_.push_back(u','); indent(); } tmpHandle_ = operationJOK_->at(runtime_, index); if (LLVM_UNLIKELY(!tmpHandle_->isString())) { // property may come from getOwnPropertyNames, which may contain numbers. // getOwnPropertyNames and propertyList_ are both only populated // with strings, numbers, and undefined only. // None of them are objects, so toString cannot throw. assert(!tmpHandle_->isObject() && "property name is an object"); auto status = toString_RJS(runtime_, tmpHandle_); assert( status != ExecutionStatus::EXCEPTION && "toString on a property cannot fail"); tmpHandle_ = status->getHermesValue(); } // tmpHandle now contains property as string. // JO.8.b.i operationQuote(StringPrimitive::createStringView( runtime_, Handle<StringPrimitive>::vmcast(tmpHandle_))); // JO.8.b.ii output_.push_back(u':'); // JO.8.b.iii if (gap_.get()) { output_.push_back(u' '); } // JO.9.a. operationStrHolder_ = vmcast<JSObject>( stackValue_->at(stackValue_->size() - 1).getObject(runtime_)); tmpHandle2_ = operationJOK_.getHermesValue(); if (PropStorage::push_back(stackJO_, runtime_, tmpHandle2_) == ExecutionStatus::EXCEPTION) { return ExecutionStatus::EXCEPTION; } // Flush just before recursion (propStoragePushBack may create handles). marker.flush(); auto result = operationStr(*tmpHandle_); operationJOK_ = vmcast<JSArray>(stackJO_->pop_back(runtime_).getObject(runtime_)); if (LLVM_UNLIKELY(result == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } if (LLVM_UNLIKELY(!result.getValue())) { // Str returns undefined, we need to roll back. output_.resize(savedLocation); } else { hasElement = true; } } // It's important to reset depthCount_ first, because the last // indent before } should be the old indent. depthCount_ = stepBack; if (hasElement) { indent(); } else { // If the object is empty, we need to roll back the first indent. output_.resize(beginningLoc); } output_.push_back(u'}'); return ExecutionStatus::RETURNED; } void JSONStringifyer::indent() { if (gap_.get()) { output_.push_back(u'\n'); for (uint32_t i = 0; i < depthCount_; ++i) { appendToOutput(gap_.get()); } } } CallResult<bool> JSONStringifyer::pushValueToStack(HermesValue value) { assert(vmisa<JSObject>(value) && "Can only push object to stack"); for (uint32_t i = 0, len = stackValue_->size(); i < len; ++i) { if (stackValue_->at(i).getObject(runtime_) == value.getObject()) { return false; } } tmpHandle_ = value; if (PropStorage::push_back(stackValue_, runtime_, tmpHandle_) == ExecutionStatus::EXCEPTION) { return ExecutionStatus::EXCEPTION; } return true; } void JSONStringifyer::popValueFromStack() { assert(stackValue_->size() && "Cannot pop from an empty stack"); stackValue_->pop_back(runtime_); } void JSONStringifyer::appendToOutput(SymbolID identifierID) { appendToOutput(runtime_.getStringPrimFromSymbolID(identifierID)); } void JSONStringifyer::appendToOutput(const StringPrimitive *str) { str->appendUTF16String(output_); } CallResult<HermesValue> JSONStringifyer::stringify(Handle<> value) { // All previous steps have been covered by the constructor. // Clear the output buffer. output_.clear(); // Step 9, 10 in ES5.1 15.12.3. operationStrHolder_ = JSObject::create(runtime_).get(); auto status = JSObject::defineOwnProperty( operationStrHolder_, runtime_, Predefined::getSymbolID(Predefined::emptyString), DefinePropertyFlags::getDefaultNewPropertyFlags(), value); assert( status != ExecutionStatus::EXCEPTION && *status && "defineOwnProperty on a newly created object cannot fail"); (void)status; // Step 11 in ES5.1 15.12.3. status = operationStr(HermesValue::encodeStringValue( runtime_.getPredefinedString(Predefined::emptyString))); if (LLVM_UNLIKELY(status == ExecutionStatus::EXCEPTION)) { return ExecutionStatus::EXCEPTION; } if (status.getValue()) { return StringPrimitive::create(runtime_, output_); } else { return HermesValue::encodeUndefinedValue(); } } CallResult<HermesValue> runtimeJSONStringify( Runtime &runtime, Handle<> value, Handle<> replacer, Handle<> space) { GCScope gcScope{runtime, "runtimeJSONStringify"}; JSONStringifyer stringifyer{runtime}; if (stringifyer.init(replacer, space) == ExecutionStatus::EXCEPTION) { return ExecutionStatus::EXCEPTION; } return stringifyer.stringify(value); } } // namespace vm } // namespace hermes