JsonSerializer.cs 19 KB

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