JsonSerializer.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611
  1. using System.Globalization;
  2. using System.Runtime.CompilerServices;
  3. using System.Text;
  4. using Jint.Collections;
  5. using Jint.Native.BigInt;
  6. using Jint.Native.Boolean;
  7. using Jint.Native.Number;
  8. using Jint.Native.Number.Dtoa;
  9. using Jint.Native.Object;
  10. using Jint.Native.Proxy;
  11. using Jint.Native.String;
  12. using Jint.Pooling;
  13. using Jint.Runtime;
  14. using Jint.Runtime.Descriptors;
  15. using Jint.Runtime.Interop;
  16. namespace Jint.Native.Json
  17. {
  18. public sealed class JsonSerializer
  19. {
  20. private readonly Engine _engine;
  21. private ObjectTraverseStack _stack = null!;
  22. private string? _indent;
  23. private string _gap = string.Empty;
  24. private List<JsValue>? _propertyList;
  25. private JsValue _replacerFunction = JsValue.Undefined;
  26. private static readonly JsString toJsonProperty = new("toJSON");
  27. public JsonSerializer(Engine engine)
  28. {
  29. _engine = engine;
  30. }
  31. public JsValue Serialize(JsValue value)
  32. {
  33. return Serialize(value, JsValue.Undefined, JsValue.Undefined);
  34. }
  35. public JsValue Serialize(JsValue value, JsValue replacer, JsValue space)
  36. {
  37. _stack = new ObjectTraverseStack(_engine);
  38. // for JSON.stringify(), any function passed as the first argument will return undefined
  39. // if the replacer is not defined. The function is not called either.
  40. if (value.IsCallable && ReferenceEquals(replacer, JsValue.Undefined))
  41. {
  42. return JsValue.Undefined;
  43. }
  44. SetupReplacer(replacer);
  45. _gap = BuildSpacingGap(space);
  46. var wrapper = _engine.Realm.Intrinsics.Object.Construct(Arguments.Empty);
  47. wrapper.DefineOwnProperty(JsString.Empty, new PropertyDescriptor(value, PropertyFlag.ConfigurableEnumerableWritable));
  48. using var jsonBuilder = StringBuilderPool.Rent();
  49. var target = new SerializerState(jsonBuilder.Builder);
  50. if (SerializeJSONProperty(JsString.Empty, wrapper, ref target) == SerializeResult.Undefined)
  51. {
  52. return JsValue.Undefined;
  53. }
  54. return new JsString(target.Json.ToString());
  55. }
  56. private void SetupReplacer(JsValue replacer)
  57. {
  58. if (replacer is not ObjectInstance oi)
  59. {
  60. return;
  61. }
  62. if (oi.IsCallable)
  63. {
  64. _replacerFunction = replacer;
  65. }
  66. else
  67. {
  68. if (oi.IsArray())
  69. {
  70. _propertyList = new List<JsValue>();
  71. var len = oi.Length;
  72. var k = 0;
  73. while (k < len)
  74. {
  75. var prop = JsString.Create(k);
  76. var v = replacer.Get(prop);
  77. var item = JsValue.Undefined;
  78. if (v.IsString())
  79. {
  80. item = v;
  81. }
  82. else if (v.IsNumber())
  83. {
  84. item = TypeConverter.ToString(v);
  85. }
  86. else if (v.IsObject())
  87. {
  88. if (v is StringInstance or NumberInstance)
  89. {
  90. item = TypeConverter.ToString(v);
  91. }
  92. }
  93. if (!item.IsUndefined() && !_propertyList.Contains(item))
  94. {
  95. _propertyList.Add(item);
  96. }
  97. k++;
  98. }
  99. }
  100. }
  101. }
  102. private static string BuildSpacingGap(JsValue space)
  103. {
  104. if (space.IsObject())
  105. {
  106. var spaceObj = space.AsObject();
  107. if (spaceObj.Class == ObjectClass.Number)
  108. {
  109. space = TypeConverter.ToNumber(spaceObj);
  110. }
  111. else if (spaceObj.Class == ObjectClass.String)
  112. {
  113. space = TypeConverter.ToJsString(spaceObj);
  114. }
  115. }
  116. // defining the gap
  117. if (space.IsNumber())
  118. {
  119. var number = ((JsNumber) space)._value;
  120. if (number > 0)
  121. {
  122. return new string(' ', (int) System.Math.Min(10, number));
  123. }
  124. return string.Empty;
  125. }
  126. if (space.IsString())
  127. {
  128. var stringSpace = space.ToString();
  129. return stringSpace.Length <= 10 ? stringSpace : stringSpace.Substring(0, 10);
  130. }
  131. return string.Empty;
  132. }
  133. /// <summary>
  134. /// https://tc39.es/ecma262/#sec-serializejsonproperty
  135. /// </summary>
  136. private SerializeResult SerializeJSONProperty(JsValue key, JsValue holder, ref SerializerState target)
  137. {
  138. var value = ReadUnwrappedValue(key, holder);
  139. if (ReferenceEquals(value, JsValue.Null))
  140. {
  141. target.Json.Append("null");
  142. return SerializeResult.NotUndefined;
  143. }
  144. if (value.IsBoolean())
  145. {
  146. target.Json.Append(((JsBoolean) value)._value ? "true" : "false");
  147. return SerializeResult.NotUndefined;
  148. }
  149. if (value.IsString())
  150. {
  151. QuoteJSONString(value.ToString(), target.Json);
  152. return SerializeResult.NotUndefined;
  153. }
  154. if (value.IsNumber())
  155. {
  156. var doubleValue = ((JsNumber) value)._value;
  157. if (value.IsInteger())
  158. {
  159. target.Json.Append((long) doubleValue);
  160. return SerializeResult.NotUndefined;
  161. }
  162. var isFinite = !double.IsNaN(doubleValue) && !double.IsInfinity(doubleValue);
  163. if (isFinite)
  164. {
  165. if (TypeConverter.CanBeStringifiedAsLong(doubleValue))
  166. {
  167. target.Json.Append((long) doubleValue);
  168. return SerializeResult.NotUndefined;
  169. }
  170. target.DtoaBuilder.Reset();
  171. NumberPrototype.NumberToString(doubleValue, target.DtoaBuilder, target.Json);
  172. return SerializeResult.NotUndefined;
  173. }
  174. target.Json.Append(JsString.NullString);
  175. return SerializeResult.NotUndefined;
  176. }
  177. if (value.IsBigInt())
  178. {
  179. ExceptionHelper.ThrowTypeError(_engine.Realm, "Do not know how to serialize a BigInt");
  180. }
  181. if (value is ObjectInstance { IsCallable: false } objectInstance)
  182. {
  183. if (CanSerializesAsArray(objectInstance))
  184. {
  185. SerializeJSONArray(objectInstance, ref target);
  186. return SerializeResult.NotUndefined;
  187. }
  188. if (objectInstance is IObjectWrapper wrapper
  189. && _engine.Options.Interop.SerializeToJson is { } serialize)
  190. {
  191. target.Json.Append(serialize(wrapper.Target));
  192. return SerializeResult.NotUndefined;
  193. }
  194. SerializeJSONObject(objectInstance, ref target);
  195. return SerializeResult.NotUndefined;
  196. }
  197. return SerializeResult.Undefined;
  198. }
  199. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  200. private JsValue ReadUnwrappedValue(JsValue key, JsValue holder)
  201. {
  202. var value = holder.Get(key);
  203. if (value._type <= InternalTypes.Integer && _replacerFunction.IsUndefined())
  204. {
  205. return value;
  206. }
  207. var isBigInt = value is BigIntInstance || value.IsBigInt();
  208. if (value.IsObject() || isBigInt)
  209. {
  210. var toJson = value.GetV(_engine.Realm, toJsonProperty);
  211. if (toJson.IsUndefined() && isBigInt)
  212. {
  213. toJson = _engine.Realm.Intrinsics.BigInt.PrototypeObject.Get(toJsonProperty);
  214. }
  215. if (toJson.IsObject())
  216. {
  217. if (toJson.AsObject() is ICallable callableToJson)
  218. {
  219. value = callableToJson.Call(value, Arguments.From(TypeConverter.ToPropertyKey(key)));
  220. }
  221. }
  222. }
  223. if (!_replacerFunction.IsUndefined())
  224. {
  225. var replacerFunctionCallable = (ICallable) _replacerFunction.AsObject();
  226. value = replacerFunctionCallable.Call(holder, Arguments.From(TypeConverter.ToPropertyKey(key), value));
  227. }
  228. if (value.IsObject())
  229. {
  230. value = value switch
  231. {
  232. NumberInstance => TypeConverter.ToNumber(value),
  233. StringInstance => TypeConverter.ToString(value),
  234. BooleanInstance booleanInstance => booleanInstance.BooleanData,
  235. BigIntInstance bigIntInstance => bigIntInstance.BigIntData,
  236. _ => value
  237. };
  238. }
  239. return value;
  240. }
  241. private static bool CanSerializesAsArray(ObjectInstance value)
  242. {
  243. if (value is JsArray)
  244. {
  245. return true;
  246. }
  247. if (value is JsProxy proxyInstance && CanSerializesAsArray(proxyInstance._target))
  248. {
  249. return true;
  250. }
  251. if (value is ObjectWrapper { IsArrayLike: true })
  252. {
  253. return true;
  254. }
  255. return false;
  256. }
  257. /// <summary>
  258. /// https://tc39.es/ecma262/#sec-quotejsonstring
  259. /// </summary>
  260. /// <remarks>
  261. /// MethodImplOptions.AggressiveOptimization = 512 which is only exposed in .NET Core.
  262. /// </remarks>
  263. [MethodImpl(MethodImplOptions.AggressiveInlining | (MethodImplOptions)512)]
  264. private static unsafe void QuoteJSONString(string value, StringBuilder target)
  265. {
  266. if (value.Length == 0)
  267. {
  268. target.Append("\"\"");
  269. return;
  270. }
  271. target.Append('"');
  272. #if NETCOREAPP1_0_OR_GREATER
  273. fixed (char* ptr = value)
  274. {
  275. int remainingLength = value.Length;
  276. int offset = 0;
  277. while (true)
  278. {
  279. int index = System.Text.Encodings.Web.JavaScriptEncoder.Default.FindFirstCharacterToEncode(ptr + offset, remainingLength);
  280. if (index < 0)
  281. {
  282. // append the remaining text which doesn't need any encoding.
  283. target.Append(value.AsSpan(offset));
  284. break;
  285. }
  286. index += offset;
  287. if (index - offset > 0)
  288. {
  289. // append everything which does not need any encoding until the found index.
  290. target.Append(value.AsSpan(offset, index - offset));
  291. }
  292. AppendJsonStringCharacter(value, ref index, target);
  293. offset = index + 1;
  294. remainingLength = value.Length - offset;
  295. if (remainingLength == 0)
  296. {
  297. break;
  298. }
  299. }
  300. }
  301. #else
  302. for (var i = 0; i < value.Length; i++)
  303. {
  304. AppendJsonStringCharacter(value, ref i, target);
  305. }
  306. #endif
  307. target.Append('"');
  308. }
  309. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  310. private static void AppendJsonStringCharacter(string value, ref int index, StringBuilder target)
  311. {
  312. var c = value[index];
  313. switch (c)
  314. {
  315. case '\"':
  316. target.Append("\\\"");
  317. break;
  318. case '\\':
  319. target.Append("\\\\");
  320. break;
  321. case '\b':
  322. target.Append("\\b");
  323. break;
  324. case '\f':
  325. target.Append("\\f");
  326. break;
  327. case '\n':
  328. target.Append("\\n");
  329. break;
  330. case '\r':
  331. target.Append("\\r");
  332. break;
  333. case '\t':
  334. target.Append("\\t");
  335. break;
  336. default:
  337. if (char.IsSurrogatePair(value, index))
  338. {
  339. #if NETCOREAPP1_0_OR_GREATER
  340. target.Append(value.AsSpan(index, 2));
  341. index++;
  342. #else
  343. target.Append(c);
  344. index++;
  345. target.Append(value[index]);
  346. #endif
  347. }
  348. else if (c < 0x20 || char.IsSurrogate(c))
  349. {
  350. target.Append("\\u");
  351. target.Append(((int) c).ToString("x4", CultureInfo.InvariantCulture));
  352. }
  353. else
  354. {
  355. target.Append(c);
  356. }
  357. break;
  358. }
  359. }
  360. /// <summary>
  361. /// https://tc39.es/ecma262/#sec-serializejsonarray
  362. /// </summary>
  363. private void SerializeJSONArray(ObjectInstance value, ref SerializerState target)
  364. {
  365. var len = TypeConverter.ToUint32(value.Get(CommonProperties.Length));
  366. if (len == 0)
  367. {
  368. target.Json.Append("[]");
  369. return;
  370. }
  371. _stack.Enter(value);
  372. var stepback = _indent;
  373. if (_gap.Length > 0)
  374. {
  375. _indent += _gap;
  376. }
  377. const char separator = ',';
  378. bool hasPrevious = false;
  379. for (int i = 0; i < len; i++)
  380. {
  381. if (hasPrevious)
  382. {
  383. target.Json.Append(separator);
  384. }
  385. else
  386. {
  387. target.Json.Append('[');
  388. }
  389. if (_gap.Length > 0)
  390. {
  391. target.Json.Append('\n');
  392. target.Json.Append(_indent);
  393. }
  394. if (SerializeJSONProperty(i, value, ref target) == SerializeResult.Undefined)
  395. {
  396. target.Json.Append(JsString.NullString);
  397. }
  398. hasPrevious = true;
  399. }
  400. if (!hasPrevious)
  401. {
  402. _stack.Exit();
  403. _indent = stepback;
  404. target.Json.Append("[]");
  405. return;
  406. }
  407. if (_gap.Length > 0)
  408. {
  409. target.Json.Append('\n');
  410. target.Json.Append(stepback);
  411. }
  412. target.Json.Append(']');
  413. _stack.Exit();
  414. _indent = stepback;
  415. }
  416. /// <summary>
  417. /// https://tc39.es/ecma262/#sec-serializejsonobject
  418. /// </summary>
  419. private void SerializeJSONObject(ObjectInstance value, ref SerializerState target)
  420. {
  421. var enumeration = _propertyList is null
  422. ? PropertyEnumeration.FromObjectInstance(value)
  423. : PropertyEnumeration.FromList(_propertyList);
  424. if (enumeration.IsEmpty)
  425. {
  426. target.Json.Append("{}");
  427. return;
  428. }
  429. _stack.Enter(value);
  430. var stepback = _indent;
  431. if (_gap.Length > 0)
  432. {
  433. _indent += _gap;
  434. }
  435. const char separator = ',';
  436. var hasPrevious = false;
  437. for (var i = 0; i < enumeration.Keys.Count; i++)
  438. {
  439. var p = enumeration.Keys[i];
  440. int position = target.Json.Length;
  441. if (hasPrevious)
  442. {
  443. target.Json.Append(separator);
  444. }
  445. else
  446. {
  447. target.Json.Append('{');
  448. }
  449. if (_gap.Length > 0)
  450. {
  451. target.Json.Append('\n');
  452. target.Json.Append(_indent);
  453. }
  454. QuoteJSONString(p.ToString(), target.Json);
  455. target.Json.Append(':');
  456. if (_gap.Length > 0)
  457. {
  458. target.Json.Append(' ');
  459. }
  460. if (SerializeJSONProperty(p, value, ref target) == SerializeResult.Undefined)
  461. {
  462. target.Json.Length = position;
  463. }
  464. else
  465. {
  466. hasPrevious = true;
  467. }
  468. }
  469. if (!hasPrevious)
  470. {
  471. _stack.Exit();
  472. _indent = stepback;
  473. target.Json.Append("{}");
  474. return;
  475. }
  476. if (_gap.Length > 0)
  477. {
  478. target.Json.Append('\n');
  479. target.Json.Append(stepback);
  480. }
  481. target.Json.Append('}');
  482. _stack.Exit();
  483. _indent = stepback;
  484. }
  485. private readonly ref struct SerializerState
  486. {
  487. public SerializerState(StringBuilder jsonBuilder)
  488. {
  489. Json = jsonBuilder;
  490. DtoaBuilder = TypeConverter.CreateDtoaBuilderForDouble();
  491. }
  492. public readonly StringBuilder Json;
  493. public readonly DtoaBuilder DtoaBuilder;
  494. }
  495. private enum SerializeResult
  496. {
  497. NotUndefined,
  498. Undefined
  499. }
  500. private readonly struct PropertyEnumeration
  501. {
  502. private PropertyEnumeration(List<JsValue> keys, bool isEmpty)
  503. {
  504. Keys = keys;
  505. IsEmpty = isEmpty;
  506. }
  507. public static PropertyEnumeration FromList(List<JsValue> keys)
  508. => new PropertyEnumeration(keys, keys.Count == 0);
  509. public static PropertyEnumeration FromObjectInstance(ObjectInstance instance)
  510. {
  511. var allKeys = instance.GetOwnPropertyKeys(Types.String);
  512. RemoveUnserializableProperties(instance, allKeys);
  513. return new PropertyEnumeration(allKeys, allKeys.Count == 0);
  514. }
  515. private static void RemoveUnserializableProperties(ObjectInstance instance, List<JsValue> keys)
  516. {
  517. for (var i = 0; i < keys.Count; i++)
  518. {
  519. var key = keys[i];
  520. var desc = instance.GetOwnProperty(key);
  521. if (desc == PropertyDescriptor.Undefined || !desc.Enumerable)
  522. {
  523. keys.RemoveAt(i);
  524. i--;
  525. }
  526. }
  527. }
  528. public readonly List<JsValue> Keys;
  529. public readonly bool IsEmpty;
  530. }
  531. }
  532. }