JSON.cpp 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954
  1. //-------------------------------------------------------------------------------------------------------
  2. // Copyright (C) Microsoft. All rights reserved.
  3. // Licensed under the MIT license. See LICENSE.txt file in the project root for full license information.
  4. //-------------------------------------------------------------------------------------------------------
  5. #include "RuntimeLibraryPch.h"
  6. #include "Library/JSONStack.h"
  7. #include "Library/JSONParser.h"
  8. #include "Library/JSON.h"
  9. #define MAX_JSON_STRINGIFY_NAMES_ON_STACK 20
  10. static const int JSONspaceSize = 10; //ES5 defined limit on the indentation space
  11. using namespace Js;
  12. namespace JSON
  13. {
  14. Js::FunctionInfo EntryInfo::Stringify(FORCE_NO_WRITE_BARRIER_TAG(JSON::Stringify), Js::FunctionInfo::ErrorOnNew);
  15. Js::FunctionInfo EntryInfo::Parse(FORCE_NO_WRITE_BARRIER_TAG(JSON::Parse), Js::FunctionInfo::ErrorOnNew);
  16. Js::Var Parse(Js::JavascriptString* input, Js::RecyclableObject* reviver, Js::ScriptContext* scriptContext);
  17. Js::Var Parse(Js::RecyclableObject* function, Js::CallInfo callInfo, ...)
  18. {
  19. PROBE_STACK(function->GetScriptContext(), Js::Constants::MinStackDefault);
  20. //ES5: parse(text [, reviver])
  21. ARGUMENTS(args, callInfo);
  22. AssertMsg(args.Info.Count > 0, "Should always have implicit 'this'");
  23. Js::ScriptContext* scriptContext = function->GetScriptContext();
  24. AUTO_TAG_NATIVE_LIBRARY_ENTRY(function, callInfo, _u("JSON.parse"));
  25. Assert(!(callInfo.Flags & Js::CallFlags_New));
  26. if(args.Info.Count < 2)
  27. {
  28. // if the text argument is missing it is assumed to be undefined.
  29. // ToString(undefined) returns "undefined" which is not a JSON grammar correct construct. Shortcut and throw here
  30. Js::JavascriptError::ThrowSyntaxError(scriptContext, ERRsyntax);
  31. }
  32. Js::JavascriptString* input;
  33. Js::Var value = args[1];
  34. if (Js::JavascriptString::Is(value))
  35. {
  36. input = Js::JavascriptString::FromVar(value);
  37. }
  38. else
  39. {
  40. input = Js::JavascriptConversion::ToString(value, scriptContext);
  41. }
  42. Js::RecyclableObject* reviver = NULL;
  43. if (args.Info.Count > 2 && Js::JavascriptConversion::IsCallable(args[2]))
  44. {
  45. reviver = Js::RecyclableObject::FromVar(args[2]);
  46. }
  47. return Parse(input, reviver, scriptContext);
  48. }
  49. Js::Var Parse(Js::JavascriptString* input, Js::RecyclableObject* reviver, Js::ScriptContext* scriptContext)
  50. {
  51. // alignment required because of the union in JSONParser::m_token
  52. __declspec (align(8)) JSONParser parser(scriptContext, reviver);
  53. Js::Var result = NULL;
  54. TryFinally([&]()
  55. {
  56. result = parser.Parse(input);
  57. #ifdef ENABLE_DEBUG_CONFIG_OPTIONS
  58. if (CONFIG_FLAG(ForceGCAfterJSONParse))
  59. {
  60. Recycler* recycler = scriptContext->GetRecycler();
  61. recycler->CollectNow<CollectNowForceInThread>();
  62. }
  63. #endif
  64. if(reviver)
  65. {
  66. Js::DynamicObject* root = scriptContext->GetLibrary()->CreateObject();
  67. JS_ETW(EventWriteJSCRIPT_RECYCLER_ALLOCATE_OBJECT(root));
  68. Js::PropertyRecord const * propertyRecord;
  69. scriptContext->GetOrAddPropertyRecord(_u(""), 0, &propertyRecord);
  70. Js::PropertyId propertyId = propertyRecord->GetPropertyId();
  71. Js::JavascriptOperators::InitProperty(root, propertyId, result);
  72. result = parser.Walk(scriptContext->GetLibrary()->GetEmptyString(), propertyId, root);
  73. }
  74. },
  75. [&](bool/*hasException*/)
  76. {
  77. parser.Finalizer();
  78. });
  79. return result;
  80. }
  81. inline bool IsValidReplacerType(Js::TypeId typeId)
  82. {
  83. switch(typeId)
  84. {
  85. case Js::TypeIds_Integer:
  86. case Js::TypeIds_String:
  87. case Js::TypeIds_Number:
  88. case Js::TypeIds_NumberObject:
  89. case Js::TypeIds_Int64Number:
  90. case Js::TypeIds_UInt64Number:
  91. case Js::TypeIds_StringObject:
  92. return true;
  93. }
  94. return false;
  95. }
  96. uint32 AddToNameTable(StringifySession::StringTable nameTable[], uint32 tableLen, uint32 size, Js::Var item, Js::ScriptContext* scriptContext)
  97. {
  98. Js::Var value = nullptr;
  99. switch (Js::JavascriptOperators::GetTypeId(item))
  100. {
  101. case Js::TypeIds_Integer:
  102. value = scriptContext->GetIntegerString(item);
  103. break;
  104. case Js::TypeIds_String:
  105. value = item;
  106. break;
  107. case Js::TypeIds_Number:
  108. case Js::TypeIds_NumberObject:
  109. case Js::TypeIds_Int64Number:
  110. case Js::TypeIds_UInt64Number:
  111. case Js::TypeIds_StringObject:
  112. value = Js::JavascriptConversion::ToString(item, scriptContext);
  113. break;
  114. }
  115. if (value && Js::JavascriptString::Is(value))
  116. {
  117. // Only validate size when about to modify it. We skip over all other (non-valid) replacement elements.
  118. if (tableLen == size)
  119. {
  120. Js::Throw::FatalInternalError(); // nameTable buffer calculation is wrong
  121. }
  122. Js::JavascriptString *propertyName = Js::JavascriptString::FromVar(value);
  123. nameTable[tableLen].propName = propertyName;
  124. Js::PropertyRecord const * propertyRecord;
  125. scriptContext->GetOrAddPropertyRecord(propertyName->GetString(), propertyName->GetLength(), &propertyRecord);
  126. nameTable[tableLen].propRecord = propertyRecord; // Keep the property id alive.
  127. tableLen++;
  128. }
  129. return tableLen;
  130. }
  131. BVSparse<ArenaAllocator>* AllocateMap(ArenaAllocator *tempAlloc)
  132. {
  133. //To escape error C2712: Cannot use __try in functions that require object unwinding
  134. return Anew(tempAlloc, BVSparse<ArenaAllocator>, tempAlloc);
  135. }
  136. Js::Var Stringify(Js::RecyclableObject* function, Js::CallInfo callInfo, ...)
  137. {
  138. PROBE_STACK(function->GetScriptContext(), Js::Constants::MinStackDefault);
  139. //ES5: Stringify(value, [replacer][, space]])
  140. ARGUMENTS(args, callInfo);
  141. Js::JavascriptLibrary* library = function->GetType()->GetLibrary();
  142. Js::ScriptContext* scriptContext = library->GetScriptContext();
  143. AUTO_TAG_NATIVE_LIBRARY_ENTRY(function, callInfo, _u("JSON.stringify"));
  144. Assert(!(callInfo.Flags & Js::CallFlags_New));
  145. if (args.Info.Count < 2)
  146. {
  147. // if value is missing it is assumed to be 'undefined'.
  148. // shortcut: the stringify algorithm returns undefined in this case.
  149. return library->GetUndefined();
  150. }
  151. Js::Var value = args[1];
  152. Js::Var replacerArg = args.Info.Count > 2 ? args[2] : nullptr;
  153. Js::Var space = args.Info.Count > 3 ? args[3] : library->GetNull();
  154. Js::DynamicObject* remoteObject;
  155. if (Js::JavascriptOperators::GetTypeId(value) == Js::TypeIds_HostDispatch)
  156. {
  157. remoteObject = Js::RecyclableObject::FromVar(value)->GetRemoteObject();
  158. if (remoteObject != nullptr)
  159. {
  160. value = Js::DynamicObject::FromVar(remoteObject);
  161. }
  162. else
  163. {
  164. Js::Var result;
  165. if (Js::RecyclableObject::FromVar(value)->InvokeBuiltInOperationRemotely(Stringify, args, &result))
  166. {
  167. return result;
  168. }
  169. }
  170. }
  171. Js::Var result = nullptr;
  172. StringifySession stringifySession(scriptContext);
  173. StringifySession::StringTable* nameTable = nullptr; //stringifySession will point to the memory allocated by nameTable, so make sure lifespans are linked.
  174. DECLARE_TEMP_GUEST_ALLOCATOR(nameTableAlloc);
  175. if (replacerArg)
  176. {
  177. if (Js::JavascriptOperators::IsArray(replacerArg))
  178. {
  179. uint32 length;
  180. Js::JavascriptArray *reArray = nullptr;
  181. Js::RecyclableObject *reRemoteArray = Js::RecyclableObject::FromVar(replacerArg);
  182. bool isArray = false;
  183. if (Js::JavascriptArray::Is(replacerArg))
  184. {
  185. reArray = Js::JavascriptArray::FromVar(replacerArg);
  186. length = reArray->GetLength();
  187. isArray = true;
  188. }
  189. else
  190. {
  191. length = Js::JavascriptConversion::ToUInt32(Js::JavascriptOperators::OP_GetLength(replacerArg, scriptContext), scriptContext);
  192. }
  193. uint32 count = 0;
  194. Js::Var item = nullptr;
  195. if (isArray)
  196. {
  197. for (uint32 i = 0; i< length; i++)
  198. {
  199. Js::TypeId idn = Js::JavascriptOperators::GetTypeId(reArray->DirectGetItem(i));
  200. if(IsValidReplacerType(idn))
  201. {
  202. count++;
  203. }
  204. }
  205. }
  206. else
  207. {
  208. for (uint32 i = 0; i< length; i++)
  209. {
  210. if (Js::JavascriptOperators::GetItem(reRemoteArray, i, &item, scriptContext))
  211. {
  212. Js::TypeId idn = Js::JavascriptOperators::GetTypeId(item);
  213. if(IsValidReplacerType(idn))
  214. {
  215. count++;
  216. }
  217. }
  218. }
  219. }
  220. uint32 tableLen = 0;
  221. if (count)
  222. {
  223. // the name table goes away with stringify session.
  224. if (count < MAX_JSON_STRINGIFY_NAMES_ON_STACK)
  225. {
  226. PROBE_STACK(scriptContext, (sizeof(StringifySession::StringTable) * count)) ;
  227. nameTable = (StringifySession::StringTable*)_alloca(sizeof(StringifySession::StringTable) * count);
  228. }
  229. else
  230. {
  231. ACQUIRE_TEMP_GUEST_ALLOCATOR(nameTableAlloc, scriptContext, _u("JSON"));
  232. nameTable = AnewArray(nameTableAlloc, StringifySession::StringTable, count);
  233. }
  234. if (isArray && !!reArray->IsCrossSiteObject())
  235. {
  236. for (uint32 i = 0; i < length; i++)
  237. {
  238. item = reArray->DirectGetItem(i);
  239. tableLen = AddToNameTable(nameTable, tableLen, count, item, scriptContext);
  240. }
  241. }
  242. else
  243. {
  244. for (uint32 i = 0; i < length; i++)
  245. {
  246. if (Js::JavascriptOperators::GetItem(reRemoteArray, i, &item, scriptContext))
  247. {
  248. tableLen = AddToNameTable(nameTable, tableLen, count, item, scriptContext);
  249. }
  250. }
  251. }
  252. //Eliminate duplicates in replacer array.
  253. BEGIN_TEMP_ALLOCATOR(tempAlloc, scriptContext, _u("JSON"))
  254. {
  255. BVSparse<ArenaAllocator>* propIdMap = AllocateMap(tempAlloc); //Anew(tempAlloc, BVSparse<ArenaAllocator>, tempAlloc);
  256. // TODO: Potential arithmetic overflow for table size/count/tableLen if large replacement args are specified.
  257. // tableLen is ensured by AddToNameTable but this doesn't propagate as an annotation so we assume here to fix the OACR warning.
  258. _Analysis_assume_(tableLen <= count);
  259. Assert(tableLen <= count);
  260. uint32 j = 0;
  261. for (uint32 i=0; i < tableLen; i++)
  262. {
  263. if(propIdMap->TestAndSet(nameTable[i].propRecord->GetPropertyId())) //Find & skip duplicate
  264. {
  265. continue;
  266. }
  267. if (j != i)
  268. {
  269. nameTable[j] = nameTable[i];
  270. }
  271. j++;
  272. }
  273. tableLen = j;
  274. }
  275. END_TEMP_ALLOCATOR(tempAlloc, scriptContext);
  276. }
  277. stringifySession.InitReplacer(nameTable, tableLen);
  278. }
  279. else if (Js::JavascriptConversion::IsCallable(replacerArg))
  280. {
  281. stringifySession.InitReplacer(Js::RecyclableObject::FromVar(replacerArg));
  282. }
  283. }
  284. BEGIN_TEMP_ALLOCATOR(tempAlloc, scriptContext, _u("JSON"))
  285. {
  286. stringifySession.CompleteInit(space, tempAlloc);
  287. Js::DynamicObject* wrapper = scriptContext->GetLibrary()->CreateObject();
  288. JS_ETW(EventWriteJSCRIPT_RECYCLER_ALLOCATE_OBJECT(wrapper));
  289. Js::PropertyRecord const * propertyRecord;
  290. scriptContext->GetOrAddPropertyRecord(_u(""), 0, &propertyRecord);
  291. Js::PropertyId propertyId = propertyRecord->GetPropertyId();
  292. Js::JavascriptOperators::InitProperty(wrapper, propertyId, value);
  293. result = stringifySession.Str(scriptContext->GetLibrary()->GetEmptyString(), propertyId, wrapper);
  294. }
  295. END_TEMP_ALLOCATOR(tempAlloc, scriptContext);
  296. RELEASE_TEMP_GUEST_ALLOCATOR(nameTableAlloc, scriptContext);
  297. return result;
  298. }
  299. // -------- StringifySession implementation ------------//
  300. void StringifySession::CompleteInit(Js::Var space, ArenaAllocator* tempAlloc)
  301. {
  302. //set the stack, gap
  303. char16 buffer[JSONspaceSize];
  304. wmemset(buffer, _u(' '), JSONspaceSize);
  305. charcount_t len = 0;
  306. switch (Js::JavascriptOperators::GetTypeId(space))
  307. {
  308. case Js::TypeIds_Integer:
  309. {
  310. len = max(0, min(JSONspaceSize, static_cast<int>(Js::TaggedInt::ToInt32(space))));
  311. break;
  312. }
  313. case Js::TypeIds_Number:
  314. case Js::TypeIds_NumberObject:
  315. case Js::TypeIds_Int64Number:
  316. case Js::TypeIds_UInt64Number:
  317. {
  318. len = max(0, static_cast<int>(min(static_cast<double>(JSONspaceSize), Js::JavascriptConversion::ToInteger(space, scriptContext))));
  319. break;
  320. }
  321. case Js::TypeIds_String:
  322. {
  323. len = min(static_cast<charcount_t>(JSONspaceSize), Js::JavascriptString::FromVar(space)->GetLength());
  324. if(len)
  325. {
  326. js_wmemcpy_s(buffer, JSONspaceSize, Js::JavascriptString::FromVar(space)->GetString(), len);
  327. }
  328. break;
  329. }
  330. case Js::TypeIds_StringObject:
  331. {
  332. Js::Var spaceString = Js::JavascriptConversion::ToString(space, scriptContext);
  333. if(Js::JavascriptString::Is(spaceString))
  334. {
  335. len = min(static_cast<charcount_t>(JSONspaceSize), Js::JavascriptString::FromVar(spaceString)->GetLength());
  336. if(len)
  337. {
  338. js_wmemcpy_s(buffer, JSONspaceSize, Js::JavascriptString::FromVar(spaceString)->GetString(), len);
  339. }
  340. }
  341. break;
  342. }
  343. }
  344. if (len)
  345. {
  346. gap = Js::JavascriptString::NewCopyBuffer(buffer, len, scriptContext);
  347. }
  348. objectStack = Anew(tempAlloc, JSONStack, tempAlloc, scriptContext);
  349. }
  350. Js::Var StringifySession::Str(uint32 index, Js::Var holder)
  351. {
  352. Js::Var value = nullptr;
  353. Js::RecyclableObject *undefined = scriptContext->GetLibrary()->GetUndefined();
  354. if (Js::JavascriptArray::Is(holder) && !Js::JavascriptArray::FromVar(holder)->IsCrossSiteObject())
  355. {
  356. if (Js::JavascriptOperators::IsUndefinedObject(value = Js::JavascriptArray::FromVar(holder)->DirectGetItem(index), undefined))
  357. {
  358. return value;
  359. }
  360. }
  361. else
  362. {
  363. Assert(Js::JavascriptOperators::IsArray(holder));
  364. Js::RecyclableObject *arr = RecyclableObject::FromVar(holder);
  365. if (!Js::JavascriptOperators::GetItem(arr, index, &value, scriptContext))
  366. {
  367. return undefined;
  368. }
  369. if (Js::JavascriptOperators::IsUndefinedObject(value, undefined))
  370. {
  371. return value;
  372. }
  373. }
  374. Js::JavascriptString *key = scriptContext->GetIntegerString(index);
  375. return StrHelper(key, value, holder);
  376. }
  377. Js::Var StringifySession::Str(Js::JavascriptString* key, Js::PropertyId keyId, Js::Var holder)
  378. {
  379. Js::Var value = nullptr;
  380. // We should look only into object's own properties here. When an object is serialized, only the own properties are considered,
  381. // the prototype chain is not considered. However, the property names can be selected via an array replacer. In this case
  382. // 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,
  383. // can end up being serialized. Well, that is the ES5 spec word.
  384. //if(!Js::RecyclableObject::FromVar(holder)->GetType()->GetProperty(holder, keyId, &value))
  385. if(!Js::JavascriptOperators::GetProperty(Js::RecyclableObject::FromVar(holder),keyId, &value, scriptContext))
  386. {
  387. return scriptContext->GetLibrary()->GetUndefined();
  388. }
  389. return StrHelper(key, value, holder);
  390. }
  391. Js::Var StringifySession::StrHelper(Js::JavascriptString* key, Js::Var value, Js::Var holder)
  392. {
  393. PROBE_STACK(scriptContext, Js::Constants::MinStackDefault);
  394. AssertMsg(Js::RecyclableObject::Is(holder), "The holder argument in a JSON::Str function must be an object");
  395. Js::Var values[3];
  396. Js::Arguments args(0, values);
  397. Js::Var undefined = scriptContext->GetLibrary()->GetUndefined();
  398. //check and apply 'toJSON' filter
  399. if (Js::JavascriptOperators::IsJsNativeObject(value) || (Js::JavascriptOperators::IsObject(value)))
  400. {
  401. Js::Var tojson = nullptr;
  402. if (Js::JavascriptOperators::GetProperty(Js::RecyclableObject::FromVar(value), Js::PropertyIds::toJSON, &tojson, scriptContext) &&
  403. Js::JavascriptConversion::IsCallable(tojson))
  404. {
  405. args.Info.Count = 2;
  406. args.Values[0] = value;
  407. args.Values[1] = key;
  408. Js::RecyclableObject* func = Js::RecyclableObject::FromVar(tojson);
  409. value = Js::JavascriptFunction::CallFunction<true>(func, func->GetEntryPoint(), args);
  410. }
  411. }
  412. //check and apply the user defined replacer filter
  413. if (ReplacerFunction == replacerType)
  414. {
  415. args.Info.Count = 3;
  416. args.Values[0] = holder;
  417. args.Values[1] = key;
  418. args.Values[2] = value;
  419. Js::RecyclableObject* func = replacer.ReplacerFunction;
  420. value = Js::JavascriptFunction::CallFunction<true>(func, func->GetEntryPoint(), args);
  421. }
  422. Js::TypeId id = Js::JavascriptOperators::GetTypeId(value);
  423. if (Js::TypeIds_NumberObject == id)
  424. {
  425. value = Js::JavascriptNumber::ToVarNoCheck(Js::JavascriptConversion::ToNumber(value, scriptContext),scriptContext);
  426. }
  427. else if (Js::TypeIds_StringObject == id)
  428. {
  429. value = Js::JavascriptConversion::ToString(value, scriptContext);
  430. }
  431. else if (Js::TypeIds_BooleanObject == id)
  432. {
  433. value = Js::JavascriptBooleanObject::FromVar(value)->GetValue() ? scriptContext->GetLibrary()->GetTrue() : scriptContext->GetLibrary()->GetFalse();
  434. }
  435. id = Js::JavascriptOperators::GetTypeId(value);
  436. switch (id)
  437. {
  438. case Js::TypeIds_Undefined:
  439. case Js::TypeIds_Symbol:
  440. return undefined;
  441. case Js::TypeIds_Null:
  442. return scriptContext->GetLibrary()->GetNullDisplayString();
  443. case Js::TypeIds_Integer:
  444. return scriptContext->GetIntegerString(value);
  445. case Js::TypeIds_Boolean:
  446. return (Js::JavascriptBoolean::FromVar(value)->GetValue()) ? scriptContext->GetLibrary()->GetTrueDisplayString() : scriptContext->GetLibrary()->GetFalseDisplayString();
  447. case Js::TypeIds_Int64Number:
  448. if (Js::NumberUtilities::IsFinite(static_cast<double>(Js::JavascriptInt64Number::FromVar(value)->GetValue())))
  449. {
  450. return Js::JavascriptConversion::ToString(value, scriptContext);
  451. }
  452. else
  453. {
  454. return scriptContext->GetLibrary()->GetNullDisplayString();
  455. }
  456. case Js::TypeIds_UInt64Number:
  457. if (Js::NumberUtilities::IsFinite(static_cast<double>(Js::JavascriptUInt64Number::FromVar(value)->GetValue())))
  458. {
  459. return Js::JavascriptConversion::ToString(value, scriptContext);
  460. }
  461. else
  462. {
  463. return scriptContext->GetLibrary()->GetNullDisplayString();
  464. }
  465. case Js::TypeIds_Number:
  466. if (Js::NumberUtilities::IsFinite(Js::JavascriptNumber::GetValue(value)))
  467. {
  468. return Js::JavascriptConversion::ToString(value, scriptContext);
  469. }
  470. else
  471. {
  472. return scriptContext->GetLibrary()->GetNullDisplayString();
  473. }
  474. case Js::TypeIds_String:
  475. return Quote(Js::JavascriptString::FromVar(value));
  476. default:
  477. Js::Var ret = undefined;
  478. if(Js::JavascriptOperators::IsJsNativeObject(value))
  479. {
  480. if (!Js::JavascriptConversion::IsCallable(value))
  481. {
  482. if (objectStack->Has(value))
  483. {
  484. Js::JavascriptError::ThrowTypeError(scriptContext, JSERR_JSONSerializeCircular);
  485. }
  486. objectStack->Push(value);
  487. if(Js::JavascriptOperators::IsArray(value))
  488. {
  489. ret = StringifyArray(value);
  490. }
  491. else
  492. {
  493. ret = StringifyObject(value);
  494. }
  495. objectStack->Pop();
  496. }
  497. }
  498. else if (Js::JavascriptOperators::IsObject(value)) //every object which is not a native object gets stringified here
  499. {
  500. if (objectStack->Has(value, false))
  501. {
  502. Js::JavascriptError::ThrowTypeError(scriptContext, JSERR_JSONSerializeCircular);
  503. }
  504. objectStack->Push(value, false);
  505. ret = StringifyObject(value);
  506. objectStack->Pop(false);
  507. }
  508. return ret;
  509. }
  510. }
  511. Js::Var StringifySession::StringifyObject(Js::Var value)
  512. {
  513. Js::JavascriptString* propertyName;
  514. Js::PropertyId id;
  515. Js::PropertyRecord const * propRecord;
  516. bool isFirstMember = true;
  517. bool isEmpty = true;
  518. uint stepBackIndent = this->indent++;
  519. Js::JavascriptString* memberSeparator = NULL; // comma or comma+linefeed+indent
  520. Js::JavascriptString* indentString = NULL; // gap*indent
  521. Js::RecyclableObject* object = Js::RecyclableObject::FromVar(value);
  522. Js::JavascriptString* result = NULL;
  523. if(ReplacerArray == this->replacerType)
  524. {
  525. result = Js::ConcatStringBuilder::New(this->scriptContext, this->replacer.propertyList.length); // Reserve initial slots for properties.
  526. for (uint k = 0; k < this->replacer.propertyList.length; k++)
  527. {
  528. propertyName = replacer.propertyList.propertyNames[k].propName;
  529. id = replacer.propertyList.propertyNames[k].propRecord->GetPropertyId();
  530. StringifyMemberObject(propertyName, id, value, (Js::ConcatStringBuilder*)result, indentString, memberSeparator, isFirstMember, isEmpty);
  531. }
  532. }
  533. else
  534. {
  535. if (JavascriptProxy::Is(object))
  536. {
  537. JavascriptProxy* proxyObject = JavascriptProxy::FromVar(object);
  538. JavascriptArray* proxyResult = proxyObject->PropertyKeysTrap(JavascriptProxy::KeysTrapKind::GetOwnPropertyNamesKind, this->scriptContext);
  539. // filter enumerable keys
  540. uint32 resultLength = proxyResult->GetLength();
  541. result = Js::ConcatStringBuilder::New(this->scriptContext, resultLength); // Reserve initial slots for properties.
  542. Var element;
  543. for (uint32 i = 0; i < resultLength; i++)
  544. {
  545. element = proxyResult->DirectGetItem(i);
  546. Assert(JavascriptString::Is(element));
  547. propertyName = JavascriptString::FromVar(element);
  548. PropertyDescriptor propertyDescriptor;
  549. JavascriptConversion::ToPropertyKey(propertyName, scriptContext, &propRecord);
  550. id = propRecord->GetPropertyId();
  551. if (JavascriptOperators::GetOwnPropertyDescriptor(RecyclableObject::FromVar(proxyObject), id, scriptContext, &propertyDescriptor))
  552. {
  553. if (propertyDescriptor.IsEnumerable())
  554. {
  555. StringifyMemberObject(propertyName, id, value, (Js::ConcatStringBuilder*)result, indentString, memberSeparator, isFirstMember, isEmpty);
  556. }
  557. }
  558. }
  559. }
  560. else
  561. {
  562. uint32 precisePropertyCount = 0;
  563. Js::JavascriptStaticEnumerator enumerator;
  564. if (object->GetEnumerator(&enumerator, EnumeratorFlags::SnapShotSemantics, scriptContext))
  565. {
  566. bool isPrecise;
  567. uint32 propertyCount = GetPropertyCount(object, &enumerator, &isPrecise);
  568. if (isPrecise)
  569. {
  570. precisePropertyCount = propertyCount;
  571. }
  572. result = Js::ConcatStringBuilder::New(this->scriptContext, propertyCount); // Reserve initial slots for properties.
  573. if (ReplacerFunction != replacerType)
  574. {
  575. enumerator.Reset();
  576. while ((propertyName = enumerator.MoveAndGetNext(id)) != NULL)
  577. {
  578. if (id == Js::Constants::NoProperty)
  579. {
  580. //if unsuccessful get propertyId from the string
  581. scriptContext->GetOrAddPropertyRecord(propertyName->GetString(), propertyName->GetLength(), &propRecord);
  582. id = propRecord->GetPropertyId();
  583. }
  584. StringifyMemberObject(propertyName, id, value, (Js::ConcatStringBuilder*)result, indentString, memberSeparator, isFirstMember, isEmpty);
  585. }
  586. }
  587. else // case: ES5 && ReplacerFunction == replacerType.
  588. {
  589. Js::Var* nameTable = nullptr;
  590. // ES5 requires that the new properties introduced by the replacer to not be stringified
  591. // Get the actual count first.
  592. if (precisePropertyCount == 0) // Check if it was updated in earlier step.
  593. {
  594. precisePropertyCount = this->GetPropertyCount(object, &enumerator);
  595. }
  596. // pick the property names before walking the object
  597. DECLARE_TEMP_GUEST_ALLOCATOR(nameTableAlloc);
  598. if (precisePropertyCount > 0)
  599. {
  600. // allocate and fill a table with the property names
  601. if (precisePropertyCount < MAX_JSON_STRINGIFY_NAMES_ON_STACK)
  602. {
  603. PROBE_STACK(scriptContext, (sizeof(Js::Var) * precisePropertyCount));
  604. nameTable = (Js::Var*)_alloca(sizeof(Js::Var) * precisePropertyCount);
  605. } else
  606. {
  607. ACQUIRE_TEMP_GUEST_ALLOCATOR(nameTableAlloc, scriptContext, _u("JSON"));
  608. nameTable = AnewArray(nameTableAlloc, Js::Var, precisePropertyCount);
  609. }
  610. enumerator.Reset();
  611. uint32 index = 0;
  612. while ((propertyName = enumerator.MoveAndGetNext(id)) != NULL && index < precisePropertyCount)
  613. {
  614. nameTable[index++] = propertyName;
  615. }
  616. // walk the property name list
  617. // Note that we're only walking up to index, not precisePropertyCount, as we only know that we've filled the array up to index
  618. for (uint k = 0; k < index; k++)
  619. {
  620. propertyName = Js::JavascriptString::FromVar(nameTable[k]);
  621. scriptContext->GetOrAddPropertyRecord(propertyName->GetString(), propertyName->GetLength(), &propRecord);
  622. id = propRecord->GetPropertyId();
  623. StringifyMemberObject(propertyName, id, value, (Js::ConcatStringBuilder*)result, indentString, memberSeparator, isFirstMember, isEmpty);
  624. }
  625. }
  626. RELEASE_TEMP_GUEST_ALLOCATOR(nameTableAlloc, scriptContext);
  627. }
  628. }
  629. }
  630. }
  631. Assert(isEmpty || result);
  632. if(isEmpty)
  633. {
  634. result = scriptContext->GetLibrary()->CreateStringFromCppLiteral(_u("{}"));
  635. }
  636. else
  637. {
  638. if(this->gap)
  639. {
  640. if(!indentString)
  641. {
  642. indentString = GetIndentString(this->indent);
  643. }
  644. // Note: it's better to use strings with length = 1 as the are cached/new instances are not created every time.
  645. Js::ConcatStringN<7>* retVal = Js::ConcatStringN<7>::New(this->scriptContext);
  646. retVal->SetItem(0, scriptContext->GetLibrary()->CreateStringFromCppLiteral(_u("{")));
  647. retVal->SetItem(1, scriptContext->GetLibrary()->CreateStringFromCppLiteral(_u("\n")));
  648. retVal->SetItem(2, indentString);
  649. retVal->SetItem(3, result);
  650. retVal->SetItem(4, scriptContext->GetLibrary()->CreateStringFromCppLiteral(_u("\n")));
  651. retVal->SetItem(5, GetIndentString(stepBackIndent));
  652. retVal->SetItem(6, scriptContext->GetLibrary()->CreateStringFromCppLiteral(_u("}")));
  653. result = retVal;
  654. }
  655. else
  656. {
  657. result = Js::ConcatStringWrapping<_u('{'), _u('}')>::New(result);
  658. }
  659. }
  660. this->indent = stepBackIndent;
  661. return result;
  662. }
  663. Js::JavascriptString* StringifySession::GetArrayElementString(uint32 index, Js::Var arrayVar)
  664. {
  665. Js::RecyclableObject *undefined = scriptContext->GetLibrary()->GetUndefined();
  666. Js::Var arrayElement = Str(index, arrayVar);
  667. if (Js::JavascriptOperators::IsUndefinedObject(arrayElement, undefined))
  668. {
  669. return scriptContext->GetLibrary()->GetNullDisplayString();
  670. }
  671. return Js::JavascriptString::FromVar(arrayElement);
  672. }
  673. Js::Var StringifySession::StringifyArray(Js::Var value)
  674. {
  675. uint stepBackIndent = this->indent++;
  676. Js::JavascriptString* memberSeparator = NULL; // comma or comma+linefeed+indent
  677. Js::JavascriptString* indentString = NULL; // gap*indent
  678. uint32 length;
  679. if (Js::JavascriptArray::Is(value))
  680. {
  681. length = Js::JavascriptArray::FromAnyArray(value)->GetLength();
  682. }
  683. else
  684. {
  685. int64 len = Js::JavascriptConversion::ToLength(Js::JavascriptOperators::OP_GetLength(value, scriptContext), scriptContext);
  686. if (MaxCharCount <= len)
  687. {
  688. // If the length goes more than MaxCharCount we will eventually fail (as OOM) in ConcatStringBuilder - so failing early.
  689. JavascriptError::ThrowRangeError(scriptContext, JSERR_OutOfBoundString);
  690. }
  691. length = (uint32)len;
  692. }
  693. Js::JavascriptString* result;
  694. if (length == 0)
  695. {
  696. result = scriptContext->GetLibrary()->CreateStringFromCppLiteral(_u("[]"));
  697. }
  698. else
  699. {
  700. if (length == 1)
  701. {
  702. result = GetArrayElementString(0, value);
  703. }
  704. else
  705. {
  706. Assert(length > 1);
  707. if (!indentString)
  708. {
  709. indentString = GetIndentString(this->indent);
  710. memberSeparator = GetMemberSeparator(indentString);
  711. }
  712. bool isFirstMember = true;
  713. // Total node count: number of array elements (N = length) + indents [including member separators] (N = length - 1).
  714. result = Js::ConcatStringBuilder::New(this->scriptContext, length * 2 - 1);
  715. for (uint32 k = 0; k < length; k++)
  716. {
  717. if (!isFirstMember)
  718. {
  719. ((Js::ConcatStringBuilder*)result)->Append(memberSeparator);
  720. }
  721. Js::JavascriptString* arrayElementString = GetArrayElementString(k, value);
  722. ((Js::ConcatStringBuilder*)result)->Append(arrayElementString);
  723. isFirstMember = false;
  724. }
  725. }
  726. if (this->gap)
  727. {
  728. if (!indentString)
  729. {
  730. indentString = GetIndentString(this->indent);
  731. }
  732. Js::ConcatStringN<6>* retVal = Js::ConcatStringN<6>::New(this->scriptContext);
  733. retVal->SetItem(0, scriptContext->GetLibrary()->CreateStringFromCppLiteral(_u("[\n")));
  734. retVal->SetItem(1, indentString);
  735. retVal->SetItem(2, result);
  736. retVal->SetItem(3, scriptContext->GetLibrary()->CreateStringFromCppLiteral(_u("\n")));
  737. retVal->SetItem(4, GetIndentString(stepBackIndent));
  738. retVal->SetItem(5, scriptContext->GetLibrary()->CreateStringFromCppLiteral(_u("]")));
  739. result = retVal;
  740. }
  741. else
  742. {
  743. result = Js::ConcatStringWrapping<_u('['), _u(']')>::New(result);
  744. }
  745. }
  746. this->indent = stepBackIndent;
  747. return result;
  748. }
  749. Js::JavascriptString* StringifySession::GetPropertySeparator()
  750. {
  751. if(!propertySeparator)
  752. {
  753. if(this->gap)
  754. {
  755. propertySeparator = scriptContext->GetLibrary()->CreateStringFromCppLiteral(_u(": "));
  756. }
  757. else
  758. {
  759. propertySeparator = scriptContext->GetLibrary()->CreateStringFromCppLiteral(_u(":"));
  760. }
  761. }
  762. return propertySeparator;
  763. }
  764. Js::JavascriptString* StringifySession::GetIndentString(uint count)
  765. {
  766. // Note: this potentially can be improved by using a special ConcatString which has gap and count fields.
  767. // Although this does not seem to be a critical path (using gap should not be often).
  768. Js::JavascriptString* res = scriptContext->GetLibrary()->CreateStringFromCppLiteral(_u(""));
  769. if(this->gap)
  770. {
  771. for (uint i = 0 ; i < count; i++)
  772. {
  773. res = Js::JavascriptString::Concat(res, this->gap);
  774. }
  775. }
  776. return res;
  777. }
  778. Js::JavascriptString* StringifySession::GetMemberSeparator(Js::JavascriptString* indentString)
  779. {
  780. if(this->gap)
  781. {
  782. return Js::JavascriptString::Concat(scriptContext->GetLibrary()->CreateStringFromCppLiteral(_u(",\n")), indentString);
  783. }
  784. else
  785. {
  786. return scriptContext->GetLibrary()->GetCommaDisplayString();
  787. }
  788. }
  789. 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 )
  790. {
  791. Js::Var propertyObjectString = Str(propertyName, id, value);
  792. if(!Js::JavascriptOperators::IsUndefinedObject(propertyObjectString, scriptContext))
  793. {
  794. int slotIndex = 0;
  795. Js::ConcatStringN<4>* tempResult = Js::ConcatStringN<4>::New(this->scriptContext); // We may use 3 or 4 slots.
  796. if(!isFirstMember)
  797. {
  798. if(!indentString)
  799. {
  800. indentString = GetIndentString(this->indent);
  801. memberSeparator = GetMemberSeparator(indentString);
  802. }
  803. tempResult->SetItem(slotIndex++, memberSeparator);
  804. }
  805. tempResult->SetItem(slotIndex++, Quote(propertyName));
  806. tempResult->SetItem(slotIndex++, this->GetPropertySeparator());
  807. tempResult->SetItem(slotIndex++, Js::JavascriptString::FromVar(propertyObjectString));
  808. result->Append(tempResult);
  809. isFirstMember = false;
  810. isEmpty = false;
  811. }
  812. }
  813. // Returns precise property count for given object and enumerator, does not count properties that are undefined.
  814. inline uint32 StringifySession::GetPropertyCount(Js::RecyclableObject* object, Js::JavascriptStaticEnumerator* enumerator)
  815. {
  816. uint32 count = 0;
  817. Js::JavascriptString * propertyName;
  818. Js::PropertyId id;
  819. enumerator->Reset();
  820. while ((propertyName = enumerator->MoveAndGetNext(id)) != NULL)
  821. {
  822. ++count;
  823. }
  824. return count;
  825. }
  826. // Returns property count (including array items) for given object and enumerator.
  827. // When object has objectArray, we do slow path return actual/precise count, in this case *pPrecise will receive true.
  828. // Otherwise we optimize for speed and try to guess the count, in this case *pPrecise will receive false.
  829. // Parameters:
  830. // - object: the object to get the number of properties for.
  831. // - enumerator: the enumerator to enumerate the object.
  832. // - [out] pIsPrecise: receives a boolean indicating whether the value returned is precise or just guessed.
  833. inline uint32 StringifySession::GetPropertyCount(Js::RecyclableObject* object, Js::JavascriptStaticEnumerator* enumerator, bool* pIsPrecise)
  834. {
  835. Assert(pIsPrecise);
  836. *pIsPrecise = false;
  837. uint32 count = object->GetPropertyCount();
  838. if (Js::DynamicObject::Is(object) && Js::DynamicObject::FromVar(object)->HasObjectArray())
  839. {
  840. // Can't use array->GetLength() as this can be sparse array for which we stringify only real/set properties.
  841. // Do one walk through the elements.
  842. // This would account for prototype property as well.
  843. count = this->GetPropertyCount(object, enumerator);
  844. *pIsPrecise = true;
  845. }
  846. if (!*pIsPrecise && count > sizeof(Js::JavascriptString*) * 8)
  847. {
  848. // For large # of elements just one more for potential prototype wouldn't matter.
  849. ++count;
  850. }
  851. return count;
  852. }
  853. inline Js::JavascriptString* StringifySession::Quote(Js::JavascriptString* value)
  854. {
  855. // By default, optimize for scenario when we don't need to change the inside of the string. That's majority of cases.
  856. return Js::JSONString::Escape<Js::EscapingOperation_NotEscape>(value);
  857. }
  858. } // namespace JSON