| //------------------------------------------------------------------------------------------------------- |
| // Copyright (C) Microsoft. All rights reserved. |
| // Licensed under the MIT license. See LICENSE.txt file in the project root for full license information. |
| //------------------------------------------------------------------------------------------------------- |
| #include "RuntimeLibraryPch.h" |
| #include "Library/JSONStack.h" |
| #include "Library/JSONParser.h" |
| #include "Library/JSON.h" |
| |
| #define MAX_JSON_STRINGIFY_NAMES_ON_STACK 20 |
| static const int JSONspaceSize = 10; //ES5 defined limit on the indentation space |
| using namespace Js; |
| |
| |
| namespace JSON |
| { |
| Js::FunctionInfo EntryInfo::Stringify(FORCE_NO_WRITE_BARRIER_TAG(JSON::Stringify), Js::FunctionInfo::ErrorOnNew); |
| Js::FunctionInfo EntryInfo::Parse(FORCE_NO_WRITE_BARRIER_TAG(JSON::Parse), Js::FunctionInfo::ErrorOnNew); |
| |
| Js::Var Parse(Js::JavascriptString* input, Js::RecyclableObject* reviver, Js::ScriptContext* scriptContext); |
| |
| Js::Var Parse(Js::RecyclableObject* function, Js::CallInfo callInfo, ...) |
| { |
| PROBE_STACK(function->GetScriptContext(), Js::Constants::MinStackDefault); |
| |
| //ES5: parse(text [, reviver]) |
| ARGUMENTS(args, callInfo); |
| AssertMsg(args.Info.Count > 0, "Should always have implicit 'this'"); |
| |
| Js::ScriptContext* scriptContext = function->GetScriptContext(); |
| AUTO_TAG_NATIVE_LIBRARY_ENTRY(function, callInfo, _u("JSON.parse")); |
| Assert(!(callInfo.Flags & Js::CallFlags_New)); |
| |
| if(args.Info.Count < 2) |
| { |
| // if the text argument is missing it is assumed to be undefined. |
| // ToString(undefined) returns "undefined" which is not a JSON grammar correct construct. Shortcut and throw here |
| Js::JavascriptError::ThrowSyntaxError(scriptContext, ERRsyntax); |
| } |
| |
| Js::JavascriptString* input; |
| Js::Var value = args[1]; |
| if (Js::JavascriptString::Is(value)) |
| { |
| input = Js::JavascriptString::FromVar(value); |
| } |
| else |
| { |
| input = Js::JavascriptConversion::ToString(value, scriptContext); |
| } |
| Js::RecyclableObject* reviver = NULL; |
| if (args.Info.Count > 2 && Js::JavascriptConversion::IsCallable(args[2])) |
| { |
| reviver = Js::RecyclableObject::FromVar(args[2]); |
| } |
| |
| return Parse(input, reviver, scriptContext); |
| } |
| |
| Js::Var Parse(Js::JavascriptString* input, Js::RecyclableObject* reviver, Js::ScriptContext* scriptContext) |
| { |
| // alignment required because of the union in JSONParser::m_token |
| __declspec (align(8)) JSONParser parser(scriptContext, reviver); |
| Js::Var result = NULL; |
| |
| TryFinally([&]() |
| { |
| result = parser.Parse(input); |
| |
| #ifdef ENABLE_DEBUG_CONFIG_OPTIONS |
| if (CONFIG_FLAG(ForceGCAfterJSONParse)) |
| { |
| Recycler* recycler = scriptContext->GetRecycler(); |
| recycler->CollectNow<CollectNowForceInThread>(); |
| } |
| #endif |
| |
| if(reviver) |
| { |
| Js::DynamicObject* root = scriptContext->GetLibrary()->CreateObject(); |
| JS_ETW(EventWriteJSCRIPT_RECYCLER_ALLOCATE_OBJECT(root)); |
| Js::PropertyRecord const * propertyRecord; |
| scriptContext->GetOrAddPropertyRecord(_u(""), 0, &propertyRecord); |
| Js::PropertyId propertyId = propertyRecord->GetPropertyId(); |
| Js::JavascriptOperators::InitProperty(root, propertyId, result); |
| result = parser.Walk(scriptContext->GetLibrary()->GetEmptyString(), propertyId, root); |
| } |
| }, |
| [&](bool/*hasException*/) |
| { |
| parser.Finalizer(); |
| }); |
| |
| return result; |
| } |
| |
| inline bool IsValidReplacerType(Js::TypeId typeId) |
| { |
| switch(typeId) |
| { |
| case Js::TypeIds_Integer: |
| case Js::TypeIds_String: |
| case Js::TypeIds_Number: |
| case Js::TypeIds_NumberObject: |
| case Js::TypeIds_Int64Number: |
| case Js::TypeIds_UInt64Number: |
| case Js::TypeIds_StringObject: |
| return true; |
| } |
| return false; |
| } |
| |
| uint32 AddToNameTable(StringifySession::StringTable nameTable[], uint32 tableLen, uint32 size, Js::Var item, Js::ScriptContext* scriptContext) |
| { |
| Js::Var value = nullptr; |
| switch (Js::JavascriptOperators::GetTypeId(item)) |
| { |
| case Js::TypeIds_Integer: |
| value = scriptContext->GetIntegerString(item); |
| break; |
| case Js::TypeIds_String: |
| value = item; |
| break; |
| case Js::TypeIds_Number: |
| case Js::TypeIds_NumberObject: |
| case Js::TypeIds_Int64Number: |
| case Js::TypeIds_UInt64Number: |
| case Js::TypeIds_StringObject: |
| value = Js::JavascriptConversion::ToString(item, scriptContext); |
| break; |
| } |
| if (value && Js::JavascriptString::Is(value)) |
| { |
| // Only validate size when about to modify it. We skip over all other (non-valid) replacement elements. |
| if (tableLen == size) |
| { |
| Js::Throw::FatalInternalError(); // nameTable buffer calculation is wrong |
| } |
| Js::JavascriptString *propertyName = Js::JavascriptString::FromVar(value); |
| nameTable[tableLen].propName = propertyName; |
| Js::PropertyRecord const * propertyRecord; |
| scriptContext->GetOrAddPropertyRecord(propertyName->GetString(), propertyName->GetLength(), &propertyRecord); |
| nameTable[tableLen].propRecord = propertyRecord; // Keep the property id alive. |
| tableLen++; |
| } |
| return tableLen; |
| } |
| |
| BVSparse<ArenaAllocator>* AllocateMap(ArenaAllocator *tempAlloc) |
| { |
| //To escape error C2712: Cannot use __try in functions that require object unwinding |
| return Anew(tempAlloc, BVSparse<ArenaAllocator>, tempAlloc); |
| } |
| |
| Js::Var Stringify(Js::RecyclableObject* function, Js::CallInfo callInfo, ...) |
| { |
| PROBE_STACK(function->GetScriptContext(), Js::Constants::MinStackDefault); |
| |
| //ES5: Stringify(value, [replacer][, space]]) |
| ARGUMENTS(args, callInfo); |
| Js::JavascriptLibrary* library = function->GetType()->GetLibrary(); |
| Js::ScriptContext* scriptContext = library->GetScriptContext(); |
| AUTO_TAG_NATIVE_LIBRARY_ENTRY(function, callInfo, _u("JSON.stringify")); |
| |
| Assert(!(callInfo.Flags & Js::CallFlags_New)); |
| |
| if (args.Info.Count < 2) |
| { |
| // if value is missing it is assumed to be 'undefined'. |
| // shortcut: the stringify algorithm returns undefined in this case. |
| return library->GetUndefined(); |
| } |
| |
| Js::Var value = args[1]; |
| Js::Var replacerArg = args.Info.Count > 2 ? args[2] : nullptr; |
| Js::Var space = args.Info.Count > 3 ? args[3] : library->GetNull(); |
| |
| Js::DynamicObject* remoteObject; |
| if (Js::JavascriptOperators::GetTypeId(value) == Js::TypeIds_HostDispatch) |
| { |
| remoteObject = Js::RecyclableObject::FromVar(value)->GetRemoteObject(); |
| if (remoteObject != nullptr) |
| { |
| value = Js::DynamicObject::FromVar(remoteObject); |
| } |
| else |
| { |
| Js::Var result; |
| if (Js::RecyclableObject::FromVar(value)->InvokeBuiltInOperationRemotely(Stringify, args, &result)) |
| { |
| return result; |
| } |
| } |
| } |
| Js::Var result = nullptr; |
| StringifySession stringifySession(scriptContext); |
| StringifySession::StringTable* nameTable = nullptr; //stringifySession will point to the memory allocated by nameTable, so make sure lifespans are linked. |
| |
| DECLARE_TEMP_GUEST_ALLOCATOR(nameTableAlloc); |
| |
| if (replacerArg) |
| { |
| if (Js::JavascriptOperators::IsArray(replacerArg)) |
| { |
| uint32 length; |
| Js::JavascriptArray *reArray = nullptr; |
| Js::RecyclableObject *reRemoteArray = Js::RecyclableObject::FromVar(replacerArg); |
| bool isArray = false; |
| |
| if (Js::JavascriptArray::Is(replacerArg)) |
| { |
| reArray = Js::JavascriptArray::FromVar(replacerArg); |
| length = reArray->GetLength(); |
| isArray = true; |
| } |
| else |
| { |
| length = Js::JavascriptConversion::ToUInt32(Js::JavascriptOperators::OP_GetLength(replacerArg, scriptContext), scriptContext); |
| } |
| |
| uint32 count = 0; |
| Js::Var item = nullptr; |
| |
| if (isArray) |
| { |
| for (uint32 i = 0; i< length; i++) |
| { |
| Js::TypeId idn = Js::JavascriptOperators::GetTypeId(reArray->DirectGetItem(i)); |
| if(IsValidReplacerType(idn)) |
| { |
| count++; |
| } |
| } |
| } |
| else |
| { |
| for (uint32 i = 0; i< length; i++) |
| { |
| if (Js::JavascriptOperators::GetItem(reRemoteArray, i, &item, scriptContext)) |
| { |
| Js::TypeId idn = Js::JavascriptOperators::GetTypeId(item); |
| if(IsValidReplacerType(idn)) |
| { |
| count++; |
| } |
| } |
| } |
| } |
| |
| uint32 tableLen = 0; |
| if (count) |
| { |
| // the name table goes away with stringify session. |
| if (count < MAX_JSON_STRINGIFY_NAMES_ON_STACK) |
| { |
| PROBE_STACK(scriptContext, (sizeof(StringifySession::StringTable) * count)) ; |
| nameTable = (StringifySession::StringTable*)_alloca(sizeof(StringifySession::StringTable) * count); |
| } |
| else |
| { |
| ACQUIRE_TEMP_GUEST_ALLOCATOR(nameTableAlloc, scriptContext, _u("JSON")); |
| nameTable = AnewArray(nameTableAlloc, StringifySession::StringTable, count); |
| } |
| if (isArray && !!reArray->IsCrossSiteObject()) |
| { |
| for (uint32 i = 0; i < length; i++) |
| { |
| item = reArray->DirectGetItem(i); |
| tableLen = AddToNameTable(nameTable, tableLen, count, item, scriptContext); |
| } |
| } |
| else |
| { |
| for (uint32 i = 0; i < length; i++) |
| { |
| if (Js::JavascriptOperators::GetItem(reRemoteArray, i, &item, scriptContext)) |
| { |
| tableLen = AddToNameTable(nameTable, tableLen, count, item, scriptContext); |
| } |
| } |
| } |
| |
| //Eliminate duplicates in replacer array. |
| BEGIN_TEMP_ALLOCATOR(tempAlloc, scriptContext, _u("JSON")) |
| { |
| BVSparse<ArenaAllocator>* propIdMap = AllocateMap(tempAlloc); //Anew(tempAlloc, BVSparse<ArenaAllocator>, tempAlloc); |
| |
| // TODO: Potential arithmetic overflow for table size/count/tableLen if large replacement args are specified. |
| // tableLen is ensured by AddToNameTable but this doesn't propagate as an annotation so we assume here to fix the OACR warning. |
| _Analysis_assume_(tableLen <= count); |
| Assert(tableLen <= count); |
| |
| uint32 j = 0; |
| for (uint32 i=0; i < tableLen; i++) |
| { |
| if(propIdMap->TestAndSet(nameTable[i].propRecord->GetPropertyId())) //Find & skip duplicate |
| { |
| continue; |
| } |
| if (j != i) |
| { |
| nameTable[j] = nameTable[i]; |
| } |
| j++; |
| } |
| tableLen = j; |
| } |
| END_TEMP_ALLOCATOR(tempAlloc, scriptContext); |
| } |
| |
| stringifySession.InitReplacer(nameTable, tableLen); |
| } |
| else if (Js::JavascriptConversion::IsCallable(replacerArg)) |
| { |
| stringifySession.InitReplacer(Js::RecyclableObject::FromVar(replacerArg)); |
| } |
| } |
| |
| BEGIN_TEMP_ALLOCATOR(tempAlloc, scriptContext, _u("JSON")) |
| { |
| stringifySession.CompleteInit(space, tempAlloc); |
| |
| Js::DynamicObject* wrapper = scriptContext->GetLibrary()->CreateObject(); |
| JS_ETW(EventWriteJSCRIPT_RECYCLER_ALLOCATE_OBJECT(wrapper)); |
| Js::PropertyRecord const * propertyRecord; |
| scriptContext->GetOrAddPropertyRecord(_u(""), 0, &propertyRecord); |
| Js::PropertyId propertyId = propertyRecord->GetPropertyId(); |
| Js::JavascriptOperators::InitProperty(wrapper, propertyId, value); |
| result = stringifySession.Str(scriptContext->GetLibrary()->GetEmptyString(), propertyId, wrapper); |
| } |
| END_TEMP_ALLOCATOR(tempAlloc, scriptContext); |
| |
| RELEASE_TEMP_GUEST_ALLOCATOR(nameTableAlloc, scriptContext); |
| return result; |
| } |
| |
| // -------- StringifySession implementation ------------// |
| |
| void StringifySession::CompleteInit(Js::Var space, ArenaAllocator* tempAlloc) |
| { |
| //set the stack, gap |
| char16 buffer[JSONspaceSize]; |
| wmemset(buffer, _u(' '), JSONspaceSize); |
| charcount_t len = 0; |
| switch (Js::JavascriptOperators::GetTypeId(space)) |
| { |
| case Js::TypeIds_Integer: |
| { |
| len = max(0, min(JSONspaceSize, static_cast<int>(Js::TaggedInt::ToInt32(space)))); |
| break; |
| } |
| case Js::TypeIds_Number: |
| case Js::TypeIds_NumberObject: |
| case Js::TypeIds_Int64Number: |
| case Js::TypeIds_UInt64Number: |
| { |
| len = max(0, static_cast<int>(min(static_cast<double>(JSONspaceSize), Js::JavascriptConversion::ToInteger(space, scriptContext)))); |
| break; |
| } |
| case Js::TypeIds_String: |
| { |
| len = min(static_cast<charcount_t>(JSONspaceSize), Js::JavascriptString::FromVar(space)->GetLength()); |
| if(len) |
| { |
| js_wmemcpy_s(buffer, JSONspaceSize, Js::JavascriptString::FromVar(space)->GetString(), len); |
| } |
| break; |
| } |
| case Js::TypeIds_StringObject: |
| { |
| Js::Var spaceString = Js::JavascriptConversion::ToString(space, scriptContext); |
| if(Js::JavascriptString::Is(spaceString)) |
| { |
| len = min(static_cast<charcount_t>(JSONspaceSize), Js::JavascriptString::FromVar(spaceString)->GetLength()); |
| if(len) |
| { |
| js_wmemcpy_s(buffer, JSONspaceSize, Js::JavascriptString::FromVar(spaceString)->GetString(), len); |
| } |
| } |
| break; |
| } |
| } |
| if (len) |
| { |
| gap = Js::JavascriptString::NewCopyBuffer(buffer, len, scriptContext); |
| } |
| |
| objectStack = Anew(tempAlloc, JSONStack, tempAlloc, scriptContext); |
| } |
| |
| Js::Var StringifySession::Str(uint32 index, Js::Var holder) |
| { |
| Js::Var value; |
| Js::RecyclableObject *undefined = scriptContext->GetLibrary()->GetUndefined(); |
| |
| if (Js::JavascriptArray::Is(holder) && !Js::JavascriptArray::FromVar(holder)->IsCrossSiteObject()) |
| { |
| if (Js::JavascriptOperators::IsUndefinedObject(value = Js::JavascriptArray::FromVar(holder)->DirectGetItem(index), undefined)) |
| { |
| return value; |
| } |
| } |
| else |
| { |
| Assert(Js::JavascriptOperators::IsArray(holder)); |
| Js::RecyclableObject *arr = RecyclableObject::FromVar(holder); |
| if (!Js::JavascriptOperators::GetItem(arr, index, &value, scriptContext)) |
| { |
| return undefined; |
| } |
| if (Js::JavascriptOperators::IsUndefinedObject(value, undefined)) |
| { |
| return value; |
| } |
| } |
| |
| Js::JavascriptString *key = scriptContext->GetIntegerString(index); |
| return StrHelper(key, value, holder); |
| } |
| |
| Js::Var StringifySession::Str(Js::JavascriptString* key, Js::PropertyId keyId, Js::Var holder) |
| { |
| Js::Var value; |
| // We should look only into object's own properties here. When an object is serialized, only the own properties are considered, |
| // the prototype chain is not considered. However, the property names can be selected via an array replacer. In this case |
| // ES5 spec doesn't say the property has to own property or even to be enumerable. So, properties from the prototype, or non enum properties, |
| // can end up being serialized. Well, that is the ES5 spec word. |
| //if(!Js::RecyclableObject::FromVar(holder)->GetType()->GetProperty(holder, keyId, &value)) |
| |
| if(!Js::JavascriptOperators::GetProperty(Js::RecyclableObject::FromVar(holder),keyId, &value, scriptContext)) |
| { |
| return scriptContext->GetLibrary()->GetUndefined(); |
| } |
| return StrHelper(key, value, holder); |
| } |
| |
| Js::Var StringifySession::StrHelper(Js::JavascriptString* key, Js::Var value, Js::Var holder) |
| { |
| PROBE_STACK(scriptContext, Js::Constants::MinStackDefault); |
| AssertMsg(Js::RecyclableObject::Is(holder), "The holder argument in a JSON::Str function must be an object"); |
| |
| Js::Var values[3]; |
| Js::Arguments args(0, values); |
| Js::Var undefined = scriptContext->GetLibrary()->GetUndefined(); |
| |
| //check and apply 'toJSON' filter |
| if (Js::JavascriptOperators::IsJsNativeObject(value) || (Js::JavascriptOperators::IsObject(value))) |
| { |
| Js::Var tojson; |
| if (Js::JavascriptOperators::GetProperty(Js::RecyclableObject::FromVar(value), Js::PropertyIds::toJSON, &tojson, scriptContext) && |
| Js::JavascriptConversion::IsCallable(tojson)) |
| { |
| args.Info.Count = 2; |
| args.Values[0] = value; |
| args.Values[1] = key; |
| |
| Js::RecyclableObject* func = Js::RecyclableObject::FromVar(tojson); |
| value = Js::JavascriptFunction::CallFunction<true>(func, func->GetEntryPoint(), args); |
| } |
| } |
| |
| //check and apply the user defined replacer filter |
| if (ReplacerFunction == replacerType) |
| { |
| args.Info.Count = 3; |
| args.Values[0] = holder; |
| args.Values[1] = key; |
| args.Values[2] = value; |
| |
| Js::RecyclableObject* func = replacer.ReplacerFunction; |
| value = Js::JavascriptFunction::CallFunction<true>(func, func->GetEntryPoint(), args); |
| } |
| |
| Js::TypeId id = Js::JavascriptOperators::GetTypeId(value); |
| if (Js::TypeIds_NumberObject == id) |
| { |
| value = Js::JavascriptNumber::ToVarNoCheck(Js::JavascriptConversion::ToNumber(value, scriptContext),scriptContext); |
| } |
| else if (Js::TypeIds_StringObject == id) |
| { |
| value = Js::JavascriptConversion::ToString(value, scriptContext); |
| } |
| else if (Js::TypeIds_BooleanObject == id) |
| { |
| value = Js::JavascriptBooleanObject::FromVar(value)->GetValue() ? scriptContext->GetLibrary()->GetTrue() : scriptContext->GetLibrary()->GetFalse(); |
| } |
| |
| id = Js::JavascriptOperators::GetTypeId(value); |
| switch (id) |
| { |
| case Js::TypeIds_Undefined: |
| case Js::TypeIds_Symbol: |
| return undefined; |
| |
| case Js::TypeIds_Null: |
| return scriptContext->GetLibrary()->GetNullDisplayString(); |
| |
| case Js::TypeIds_Integer: |
| return scriptContext->GetIntegerString(value); |
| |
| case Js::TypeIds_Boolean: |
| return (Js::JavascriptBoolean::FromVar(value)->GetValue()) ? scriptContext->GetLibrary()->GetTrueDisplayString() : scriptContext->GetLibrary()->GetFalseDisplayString(); |
| |
| case Js::TypeIds_Int64Number: |
| if (Js::NumberUtilities::IsFinite(static_cast<double>(Js::JavascriptInt64Number::FromVar(value)->GetValue()))) |
| { |
| return Js::JavascriptConversion::ToString(value, scriptContext); |
| } |
| else |
| { |
| return scriptContext->GetLibrary()->GetNullDisplayString(); |
| } |
| |
| case Js::TypeIds_UInt64Number: |
| if (Js::NumberUtilities::IsFinite(static_cast<double>(Js::JavascriptUInt64Number::FromVar(value)->GetValue()))) |
| { |
| return Js::JavascriptConversion::ToString(value, scriptContext); |
| } |
| else |
| { |
| return scriptContext->GetLibrary()->GetNullDisplayString(); |
| } |
| |
| case Js::TypeIds_Number: |
| if (Js::NumberUtilities::IsFinite(Js::JavascriptNumber::GetValue(value))) |
| { |
| return Js::JavascriptConversion::ToString(value, scriptContext); |
| } |
| else |
| { |
| return scriptContext->GetLibrary()->GetNullDisplayString(); |
| } |
| |
| case Js::TypeIds_String: |
| return Quote(Js::JavascriptString::FromVar(value)); |
| |
| default: |
| Js::Var ret = undefined; |
| if(Js::JavascriptOperators::IsJsNativeObject(value)) |
| { |
| if (!Js::JavascriptConversion::IsCallable(value)) |
| { |
| if (objectStack->Has(value)) |
| { |
| Js::JavascriptError::ThrowTypeError(scriptContext, JSERR_JSONSerializeCircular); |
| } |
| objectStack->Push(value); |
| |
| if(Js::JavascriptOperators::IsArray(value)) |
| { |
| ret = StringifyArray(value); |
| } |
| else |
| { |
| ret = StringifyObject(value); |
| } |
| objectStack->Pop(); |
| } |
| } |
| else if (Js::JavascriptOperators::IsObject(value)) //every object which is not a native object gets stringified here |
| { |
| if (objectStack->Has(value, false)) |
| { |
| Js::JavascriptError::ThrowTypeError(scriptContext, JSERR_JSONSerializeCircular); |
| } |
| objectStack->Push(value, false); |
| ret = StringifyObject(value); |
| objectStack->Pop(false); |
| } |
| return ret; |
| } |
| } |
| |
| Js::Var StringifySession::StringifyObject(Js::Var value) |
| { |
| Js::JavascriptString* propertyName; |
| Js::PropertyId id; |
| Js::PropertyRecord const * propRecord; |
| |
| bool isFirstMember = true; |
| bool isEmpty = true; |
| |
| uint stepBackIndent = this->indent++; |
| Js::JavascriptString* memberSeparator = NULL; // comma or comma+linefeed+indent |
| Js::JavascriptString* indentString = NULL; // gap*indent |
| Js::RecyclableObject* object = Js::RecyclableObject::FromVar(value); |
| Js::JavascriptString* result = NULL; |
| |
| if(ReplacerArray == this->replacerType) |
| { |
| result = Js::ConcatStringBuilder::New(this->scriptContext, this->replacer.propertyList.length); // Reserve initial slots for properties. |
| |
| for (uint k = 0; k < this->replacer.propertyList.length; k++) |
| { |
| propertyName = replacer.propertyList.propertyNames[k].propName; |
| id = replacer.propertyList.propertyNames[k].propRecord->GetPropertyId(); |
| |
| StringifyMemberObject(propertyName, id, value, (Js::ConcatStringBuilder*)result, indentString, memberSeparator, isFirstMember, isEmpty); |
| } |
| } |
| else |
| { |
| if (JavascriptProxy::Is(object)) |
| { |
| JavascriptProxy* proxyObject = JavascriptProxy::FromVar(object); |
| JavascriptArray* proxyResult = proxyObject->PropertyKeysTrap(JavascriptProxy::KeysTrapKind::GetOwnPropertyNamesKind, this->scriptContext); |
| |
| // filter enumerable keys |
| uint32 resultLength = proxyResult->GetLength(); |
| result = Js::ConcatStringBuilder::New(this->scriptContext, resultLength); // Reserve initial slots for properties. |
| Var element; |
| for (uint32 i = 0; i < resultLength; i++) |
| { |
| element = proxyResult->DirectGetItem(i); |
| |
| Assert(JavascriptString::Is(element)); |
| propertyName = JavascriptString::FromVar(element); |
| |
| PropertyDescriptor propertyDescriptor; |
| JavascriptConversion::ToPropertyKey(propertyName, scriptContext, &propRecord); |
| id = propRecord->GetPropertyId(); |
| if (JavascriptOperators::GetOwnPropertyDescriptor(RecyclableObject::FromVar(proxyObject), id, scriptContext, &propertyDescriptor)) |
| { |
| if (propertyDescriptor.IsEnumerable()) |
| { |
| StringifyMemberObject(propertyName, id, value, (Js::ConcatStringBuilder*)result, indentString, memberSeparator, isFirstMember, isEmpty); |
| } |
| } |
| } |
| } |
| else |
| { |
| uint32 precisePropertyCount = 0; |
| Js::JavascriptStaticEnumerator enumerator; |
| if (object->GetEnumerator(&enumerator, EnumeratorFlags::SnapShotSemantics, scriptContext)) |
| { |
| bool isPrecise; |
| uint32 propertyCount = GetPropertyCount(object, &enumerator, &isPrecise); |
| if (isPrecise) |
| { |
| precisePropertyCount = propertyCount; |
| } |
| |
| result = Js::ConcatStringBuilder::New(this->scriptContext, propertyCount); // Reserve initial slots for properties. |
| |
| if (ReplacerFunction != replacerType) |
| { |
| enumerator.Reset(); |
| while ((propertyName = enumerator.MoveAndGetNext(id)) != NULL) |
| { |
| if (id == Js::Constants::NoProperty) |
| { |
| //if unsuccessful get propertyId from the string |
| scriptContext->GetOrAddPropertyRecord(propertyName->GetString(), propertyName->GetLength(), &propRecord); |
| id = propRecord->GetPropertyId(); |
| } |
| StringifyMemberObject(propertyName, id, value, (Js::ConcatStringBuilder*)result, indentString, memberSeparator, isFirstMember, isEmpty); |
| } |
| } |
| else // case: ES5 && ReplacerFunction == replacerType. |
| { |
| Js::Var* nameTable = nullptr; |
| // ES5 requires that the new properties introduced by the replacer to not be stringified |
| // Get the actual count first. |
| if (precisePropertyCount == 0) // Check if it was updated in earlier step. |
| { |
| precisePropertyCount = this->GetPropertyCount(object, &enumerator); |
| } |
| |
| // pick the property names before walking the object |
| DECLARE_TEMP_GUEST_ALLOCATOR(nameTableAlloc); |
| if (precisePropertyCount > 0) |
| { |
| // allocate and fill a table with the property names |
| if (precisePropertyCount < MAX_JSON_STRINGIFY_NAMES_ON_STACK) |
| { |
| PROBE_STACK(scriptContext, (sizeof(Js::Var) * precisePropertyCount)); |
| nameTable = (Js::Var*)_alloca(sizeof(Js::Var) * precisePropertyCount); |
| } else |
| { |
| ACQUIRE_TEMP_GUEST_ALLOCATOR(nameTableAlloc, scriptContext, _u("JSON")); |
| nameTable = AnewArray(nameTableAlloc, Js::Var, precisePropertyCount); |
| } |
| enumerator.Reset(); |
| uint32 index = 0; |
| while ((propertyName = enumerator.MoveAndGetNext(id)) != NULL && index < precisePropertyCount) |
| { |
| nameTable[index++] = propertyName; |
| } |
| |
| // walk the property name list |
| for (uint k = 0; k < precisePropertyCount; k++) |
| { |
| propertyName = Js::JavascriptString::FromVar(nameTable[k]); |
| scriptContext->GetOrAddPropertyRecord(propertyName->GetString(), propertyName->GetLength(), &propRecord); |
| id = propRecord->GetPropertyId(); |
| StringifyMemberObject(propertyName, id, value, (Js::ConcatStringBuilder*)result, indentString, memberSeparator, isFirstMember, isEmpty); |
| } |
| } |
| RELEASE_TEMP_GUEST_ALLOCATOR(nameTableAlloc, scriptContext); |
| } |
| } |
| } |
| } |
| Assert(isEmpty || result); |
| |
| if(isEmpty) |
| { |
| result = scriptContext->GetLibrary()->CreateStringFromCppLiteral(_u("{}")); |
| } |
| else |
| { |
| if(this->gap) |
| { |
| if(!indentString) |
| { |
| indentString = GetIndentString(this->indent); |
| } |
| // Note: it's better to use strings with length = 1 as the are cached/new instances are not created every time. |
| Js::ConcatStringN<7>* retVal = Js::ConcatStringN<7>::New(this->scriptContext); |
| retVal->SetItem(0, scriptContext->GetLibrary()->CreateStringFromCppLiteral(_u("{"))); |
| retVal->SetItem(1, scriptContext->GetLibrary()->CreateStringFromCppLiteral(_u("\n"))); |
| retVal->SetItem(2, indentString); |
| retVal->SetItem(3, result); |
| retVal->SetItem(4, scriptContext->GetLibrary()->CreateStringFromCppLiteral(_u("\n"))); |
| retVal->SetItem(5, GetIndentString(stepBackIndent)); |
| retVal->SetItem(6, scriptContext->GetLibrary()->CreateStringFromCppLiteral(_u("}"))); |
| result = retVal; |
| } |
| else |
| { |
| result = Js::ConcatStringWrapping<_u('{'), _u('}')>::New(result); |
| } |
| } |
| |
| this->indent = stepBackIndent; |
| return result; |
| } |
| |
| Js::JavascriptString* StringifySession::GetArrayElementString(uint32 index, Js::Var arrayVar) |
| { |
| Js::RecyclableObject *undefined = scriptContext->GetLibrary()->GetUndefined(); |
| |
| Js::Var arrayElement = Str(index, arrayVar); |
| if (Js::JavascriptOperators::IsUndefinedObject(arrayElement, undefined)) |
| { |
| return scriptContext->GetLibrary()->GetNullDisplayString(); |
| } |
| return Js::JavascriptString::FromVar(arrayElement); |
| } |
| |
| Js::Var StringifySession::StringifyArray(Js::Var value) |
| { |
| uint stepBackIndent = this->indent++; |
| Js::JavascriptString* memberSeparator = NULL; // comma or comma+linefeed+indent |
| Js::JavascriptString* indentString = NULL; // gap*indent |
| |
| uint32 length; |
| |
| if (Js::JavascriptArray::Is(value)) |
| { |
| length = Js::JavascriptArray::FromAnyArray(value)->GetLength(); |
| } |
| else |
| { |
| int64 len = Js::JavascriptConversion::ToLength(Js::JavascriptOperators::OP_GetLength(value, scriptContext), scriptContext); |
| if (MaxCharCount <= len) |
| { |
| // If the length goes more than MaxCharCount we will eventually fail (as OOM) in ConcatStringBuilder - so failing early. |
| JavascriptError::ThrowRangeError(scriptContext, JSERR_OutOfBoundString); |
| } |
| length = (uint32)len; |
| } |
| |
| Js::JavascriptString* result; |
| if (length == 0) |
| { |
| result = scriptContext->GetLibrary()->CreateStringFromCppLiteral(_u("[]")); |
| } |
| else |
| { |
| if (length == 1) |
| { |
| result = GetArrayElementString(0, value); |
| } |
| else |
| { |
| Assert(length > 1); |
| if (!indentString) |
| { |
| indentString = GetIndentString(this->indent); |
| memberSeparator = GetMemberSeparator(indentString); |
| } |
| bool isFirstMember = true; |
| |
| // Total node count: number of array elements (N = length) + indents [including member separators] (N = length - 1). |
| result = Js::ConcatStringBuilder::New(this->scriptContext, length * 2 - 1); |
| for (uint32 k = 0; k < length; k++) |
| { |
| if (!isFirstMember) |
| { |
| ((Js::ConcatStringBuilder*)result)->Append(memberSeparator); |
| } |
| Js::JavascriptString* arrayElementString = GetArrayElementString(k, value); |
| ((Js::ConcatStringBuilder*)result)->Append(arrayElementString); |
| isFirstMember = false; |
| } |
| } |
| |
| if (this->gap) |
| { |
| if (!indentString) |
| { |
| indentString = GetIndentString(this->indent); |
| } |
| Js::ConcatStringN<6>* retVal = Js::ConcatStringN<6>::New(this->scriptContext); |
| retVal->SetItem(0, scriptContext->GetLibrary()->CreateStringFromCppLiteral(_u("[\n"))); |
| retVal->SetItem(1, indentString); |
| retVal->SetItem(2, result); |
| retVal->SetItem(3, scriptContext->GetLibrary()->CreateStringFromCppLiteral(_u("\n"))); |
| retVal->SetItem(4, GetIndentString(stepBackIndent)); |
| retVal->SetItem(5, scriptContext->GetLibrary()->CreateStringFromCppLiteral(_u("]"))); |
| result = retVal; |
| } |
| else |
| { |
| result = Js::ConcatStringWrapping<_u('['), _u(']')>::New(result); |
| } |
| } |
| |
| this->indent = stepBackIndent; |
| return result; |
| } |
| |
| Js::JavascriptString* StringifySession::GetPropertySeparator() |
| { |
| if(!propertySeparator) |
| { |
| if(this->gap) |
| { |
| propertySeparator = scriptContext->GetLibrary()->CreateStringFromCppLiteral(_u(": ")); |
| } |
| else |
| { |
| propertySeparator = scriptContext->GetLibrary()->CreateStringFromCppLiteral(_u(":")); |
| } |
| } |
| return propertySeparator; |
| } |
| |
| Js::JavascriptString* StringifySession::GetIndentString(uint count) |
| { |
| // Note: this potentially can be improved by using a special ConcatString which has gap and count fields. |
| // Although this does not seem to be a critical path (using gap should not be often). |
| Js::JavascriptString* res = scriptContext->GetLibrary()->CreateStringFromCppLiteral(_u("")); |
| if(this->gap) |
| { |
| for (uint i = 0 ; i < count; i++) |
| { |
| res = Js::JavascriptString::Concat(res, this->gap); |
| } |
| } |
| return res; |
| } |
| |
| Js::JavascriptString* StringifySession::GetMemberSeparator(Js::JavascriptString* indentString) |
| { |
| if(this->gap) |
| { |
| return Js::JavascriptString::Concat(scriptContext->GetLibrary()->CreateStringFromCppLiteral(_u(",\n")), indentString); |
| } |
| else |
| { |
| return scriptContext->GetLibrary()->GetCommaDisplayString(); |
| } |
| } |
| |
| void StringifySession::StringifyMemberObject( Js::JavascriptString* propertyName, Js::PropertyId id, Js::Var value, Js::ConcatStringBuilder* result, Js::JavascriptString* &indentString, Js::JavascriptString* &memberSeparator, bool &isFirstMember, bool &isEmpty ) |
| { |
| Js::Var propertyObjectString = Str(propertyName, id, value); |
| if(!Js::JavascriptOperators::IsUndefinedObject(propertyObjectString, scriptContext)) |
| { |
| int slotIndex = 0; |
| Js::ConcatStringN<4>* tempResult = Js::ConcatStringN<4>::New(this->scriptContext); // We may use 3 or 4 slots. |
| if(!isFirstMember) |
| { |
| if(!indentString) |
| { |
| indentString = GetIndentString(this->indent); |
| memberSeparator = GetMemberSeparator(indentString); |
| } |
| tempResult->SetItem(slotIndex++, memberSeparator); |
| } |
| tempResult->SetItem(slotIndex++, Quote(propertyName)); |
| tempResult->SetItem(slotIndex++, this->GetPropertySeparator()); |
| tempResult->SetItem(slotIndex++, Js::JavascriptString::FromVar(propertyObjectString)); |
| |
| result->Append(tempResult); |
| isFirstMember = false; |
| isEmpty = false; |
| } |
| } |
| |
| // Returns precise property count for given object and enumerator, does not count properties that are undefined. |
| inline uint32 StringifySession::GetPropertyCount(Js::RecyclableObject* object, Js::JavascriptStaticEnumerator* enumerator) |
| { |
| uint32 count = 0; |
| Js::JavascriptString * propertyName; |
| Js::PropertyId id; |
| enumerator->Reset(); |
| while ((propertyName = enumerator->MoveAndGetNext(id)) != NULL) |
| { |
| ++count; |
| } |
| return count; |
| } |
| |
| // Returns property count (including array items) for given object and enumerator. |
| // When object has objectArray, we do slow path return actual/precise count, in this case *pPrecise will receive true. |
| // Otherwise we optimize for speed and try to guess the count, in this case *pPrecise will receive false. |
| // Parameters: |
| // - object: the object to get the number of properties for. |
| // - enumerator: the enumerator to enumerate the object. |
| // - [out] pIsPrecise: receives a boolean indicating whether the value returned is precise or just guessed. |
| inline uint32 StringifySession::GetPropertyCount(Js::RecyclableObject* object, Js::JavascriptStaticEnumerator* enumerator, bool* pIsPrecise) |
| { |
| Assert(pIsPrecise); |
| *pIsPrecise = false; |
| |
| uint32 count = object->GetPropertyCount(); |
| if (Js::DynamicObject::Is(object) && Js::DynamicObject::FromVar(object)->HasObjectArray()) |
| { |
| // Can't use array->GetLength() as this can be sparse array for which we stringify only real/set properties. |
| // Do one walk through the elements. |
| // This would account for prototype property as well. |
| count = this->GetPropertyCount(object, enumerator); |
| *pIsPrecise = true; |
| } |
| if (!*pIsPrecise && count > sizeof(Js::JavascriptString*) * 8) |
| { |
| // For large # of elements just one more for potential prototype wouldn't matter. |
| ++count; |
| } |
| |
| return count; |
| } |
| |
| inline Js::JavascriptString* StringifySession::Quote(Js::JavascriptString* value) |
| { |
| // By default, optimize for scenario when we don't need to change the inside of the string. That's majority of cases. |
| return Js::JSONString::Escape<Js::EscapingOperation_NotEscape>(value); |
| } |
| } // namespace JSON |