using System.Globalization; using System.Runtime.CompilerServices; using System.Text; using Jint.Collections; using Jint.Native.BigInt; using Jint.Native.Boolean; using Jint.Native.Number; using Jint.Native.Object; using Jint.Native.String; using Jint.Runtime; using Jint.Runtime.Descriptors; using Jint.Runtime.Interop; namespace Jint.Native.Json { public sealed class JsonSerializer { private readonly Engine _engine; private ObjectTraverseStack _stack = null!; private string? _indent; private string _gap = string.Empty; private List? _propertyList; private JsValue _replacerFunction = JsValue.Undefined; private static readonly JsString toJsonProperty = new("toJSON"); public JsonSerializer(Engine engine) { _engine = engine; } public JsValue Serialize(JsValue value) { return Serialize(value, JsValue.Undefined, JsValue.Undefined); } public JsValue Serialize(JsValue value, JsValue replacer, JsValue space) { _stack = new ObjectTraverseStack(_engine); // for JSON.stringify(), any function passed as the first argument will return undefined // if the replacer is not defined. The function is not called either. if (value.IsCallable && ReferenceEquals(replacer, JsValue.Undefined)) { return JsValue.Undefined; } SetupReplacer(replacer); _gap = BuildSpacingGap(space); var wrapper = _engine.Realm.Intrinsics.Object.Construct(Arguments.Empty); wrapper.DefineOwnProperty(JsString.Empty, new PropertyDescriptor(value, PropertyFlag.ConfigurableEnumerableWritable)); string result; var json = new ValueStringBuilder(); try { if (SerializeJSONProperty(JsString.Empty, wrapper, ref json) == SerializeResult.Undefined) { return JsValue.Undefined; } } finally { result = json.ToString(); } return new JsString(result); } private void SetupReplacer(JsValue replacer) { if (replacer is not ObjectInstance oi) { return; } if (oi.IsCallable) { _replacerFunction = replacer; } else { if (oi.IsArray()) { _propertyList = new List(); var len = oi.GetLength(); var k = 0; while (k < len) { var prop = JsString.Create(k); var v = replacer.Get(prop); var item = JsValue.Undefined; if (v.IsString()) { item = v; } else if (v.IsNumber()) { item = TypeConverter.ToString(v); } else if (v.IsObject()) { if (v is StringInstance or NumberInstance) { item = TypeConverter.ToString(v); } } if (!item.IsUndefined() && !_propertyList.Contains(item)) { _propertyList.Add(item); } k++; } } } } private static string BuildSpacingGap(JsValue space) { if (space.IsObject()) { var spaceObj = space.AsObject(); if (spaceObj.Class == ObjectClass.Number) { space = TypeConverter.ToNumber(spaceObj); } else if (spaceObj.Class == ObjectClass.String) { space = TypeConverter.ToJsString(spaceObj); } } // defining the gap if (space.IsNumber()) { var number = ((JsNumber) space)._value; if (number > 0) { return new string(' ', (int) System.Math.Min(10, number)); } return string.Empty; } if (space.IsString()) { var stringSpace = space.ToString(); return stringSpace.Length <= 10 ? stringSpace : stringSpace.Substring(0, 10); } return string.Empty; } /// /// https://tc39.es/ecma262/#sec-serializejsonproperty /// private SerializeResult SerializeJSONProperty(JsValue key, JsValue holder, ref ValueStringBuilder json) { var value = ReadUnwrappedValue(key, holder); if (ReferenceEquals(value, JsValue.Null)) { json.Append("null"); return SerializeResult.NotUndefined; } if (value.IsBoolean()) { json.Append(((JsBoolean) value)._value ? "true" : "false"); return SerializeResult.NotUndefined; } if (value.IsString()) { QuoteJSONString(value.ToString(), ref json); return SerializeResult.NotUndefined; } if (value.IsNumber()) { var doubleValue = ((JsNumber) value)._value; if (value.IsInteger()) { json.Append(((long) doubleValue).ToString(CultureInfo.InvariantCulture)); return SerializeResult.NotUndefined; } var isFinite = !double.IsNaN(doubleValue) && !double.IsInfinity(doubleValue); if (isFinite) { if (TypeConverter.CanBeStringifiedAsLong(doubleValue)) { json.Append(((long) doubleValue).ToString(CultureInfo.InvariantCulture)); return SerializeResult.NotUndefined; } json.Append(NumberPrototype.ToNumberString(doubleValue)); return SerializeResult.NotUndefined; } json.Append("null"); return SerializeResult.NotUndefined; } if (value.IsBigInt()) { ExceptionHelper.ThrowTypeError(_engine.Realm, "Do not know how to serialize a BigInt"); } if (value is ObjectInstance { IsCallable: false } objectInstance) { if (CanSerializesAsArray(objectInstance)) { SerializeJSONArray(objectInstance, ref json); return SerializeResult.NotUndefined; } if (objectInstance is IObjectWrapper wrapper && _engine.Options.Interop.SerializeToJson is { } serialize) { json.Append(serialize(wrapper.Target)); return SerializeResult.NotUndefined; } SerializeJSONObject(objectInstance, ref json); return SerializeResult.NotUndefined; } return SerializeResult.Undefined; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private JsValue ReadUnwrappedValue(JsValue key, JsValue holder) { var value = holder.Get(key); if (value._type <= InternalTypes.Integer && _replacerFunction.IsUndefined()) { return value; } var isBigInt = value is BigIntInstance || value.IsBigInt(); if (value.IsObject() || isBigInt) { var toJson = value.GetV(_engine.Realm, toJsonProperty); if (toJson.IsUndefined() && isBigInt) { toJson = _engine.Realm.Intrinsics.BigInt.PrototypeObject.Get(toJsonProperty); } if (toJson.IsObject()) { if (toJson.AsObject() is ICallable callableToJson) { value = callableToJson.Call(value, Arguments.From(TypeConverter.ToPropertyKey(key))); } } } if (!_replacerFunction.IsUndefined()) { var replacerFunctionCallable = (ICallable) _replacerFunction.AsObject(); value = replacerFunctionCallable.Call(holder, Arguments.From(TypeConverter.ToPropertyKey(key), value)); } if (value.IsObject()) { value = value switch { NumberInstance => TypeConverter.ToNumber(value), StringInstance => TypeConverter.ToString(value), BooleanInstance booleanInstance => booleanInstance.BooleanData, BigIntInstance bigIntInstance => bigIntInstance.BigIntData, _ => value }; } return value; } private static bool CanSerializesAsArray(ObjectInstance value) { if (value is JsArray) { return true; } if (value is JsProxy proxyInstance && CanSerializesAsArray(proxyInstance._target)) { return true; } if (value is ObjectWrapper { IsArrayLike: true }) { return true; } return false; } /// /// https://tc39.es/ecma262/#sec-quotejsonstring /// /// /// MethodImplOptions.AggressiveOptimization = 512 which is only exposed in .NET Core. /// [MethodImpl(MethodImplOptions.AggressiveInlining | (MethodImplOptions) 512)] private static unsafe void QuoteJSONString(string value, ref ValueStringBuilder json) { if (value.Length == 0) { json.Append("\"\""); return; } json.Append('"'); #if NETCOREAPP1_0_OR_GREATER fixed (char* ptr = value) { int remainingLength = value.Length; int offset = 0; while (true) { int index = System.Text.Encodings.Web.JavaScriptEncoder.Default.FindFirstCharacterToEncode(ptr + offset, remainingLength); if (index < 0) { // append the remaining text which doesn't need any encoding. json.Append(value.AsSpan(offset)); break; } index += offset; if (index - offset > 0) { // append everything which does not need any encoding until the found index. json.Append(value.AsSpan(offset, index - offset)); } AppendJsonStringCharacter(value, ref index, ref json); offset = index + 1; remainingLength = value.Length - offset; if (remainingLength == 0) { break; } } } #else for (var i = 0; i < value.Length; i++) { AppendJsonStringCharacter(value, ref i, ref json); } #endif json.Append('"'); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void AppendJsonStringCharacter(string value, ref int index, ref ValueStringBuilder json) { var c = value[index]; switch (c) { case '\"': json.Append("\\\""); break; case '\\': json.Append("\\\\"); break; case '\b': json.Append("\\b"); break; case '\f': json.Append("\\f"); break; case '\n': json.Append("\\n"); break; case '\r': json.Append("\\r"); break; case '\t': json.Append("\\t"); break; default: if (char.IsSurrogatePair(value, index)) { #if NETCOREAPP1_0_OR_GREATER json.Append(value.AsSpan(index, 2)); index++; #else json.Append(c); index++; json.Append(value[index]); #endif } else if (c < 0x20 || char.IsSurrogate(c)) { json.Append("\\u"); json.Append(((int) c).ToString("x4", CultureInfo.InvariantCulture)); } else { json.Append(c); } break; } } /// /// https://tc39.es/ecma262/#sec-serializejsonarray /// private void SerializeJSONArray(ObjectInstance value, ref ValueStringBuilder json) { var len = TypeConverter.ToUint32(value.Get(CommonProperties.Length)); if (len == 0) { json.Append("[]"); return; } _stack.Enter(value); var stepback = _indent; if (_gap.Length > 0) { _indent += _gap; } const char separator = ','; bool hasPrevious = false; for (int i = 0; i < len; i++) { if (hasPrevious) { json.Append(separator); } else { json.Append('['); } if (_gap.Length > 0) { json.Append('\n'); json.Append(_indent); } if (SerializeJSONProperty(i, value, ref json) == SerializeResult.Undefined) { json.Append("null"); } hasPrevious = true; } if (!hasPrevious) { _stack.Exit(); _indent = stepback; json.Append("[]"); return; } if (_gap.Length > 0) { json.Append('\n'); json.Append(stepback); } json.Append(']'); _stack.Exit(); _indent = stepback; } /// /// https://tc39.es/ecma262/#sec-serializejsonobject /// private void SerializeJSONObject(ObjectInstance value, ref ValueStringBuilder json) { var enumeration = _propertyList is null ? PropertyEnumeration.FromObjectInstance(value) : PropertyEnumeration.FromList(_propertyList); if (enumeration.IsEmpty) { json.Append("{}"); return; } _stack.Enter(value); var stepback = _indent; if (_gap.Length > 0) { _indent += _gap; } const char separator = ','; var hasPrevious = false; for (var i = 0; i < enumeration.Keys.Count; i++) { var p = enumeration.Keys[i]; int position = json.Length; if (hasPrevious) { json.Append(separator); } else { json.Append('{'); } if (_gap.Length > 0) { json.Append('\n'); json.Append(_indent); } QuoteJSONString(p.ToString(), ref json); json.Append(':'); if (_gap.Length > 0) { json.Append(' '); } if (SerializeJSONProperty(p, value, ref json) == SerializeResult.Undefined) { json.Length = position; } else { hasPrevious = true; } } if (!hasPrevious) { _stack.Exit(); _indent = stepback; json.Append("{}"); return; } if (_gap.Length > 0) { json.Append('\n'); json.Append(stepback); } json.Append('}'); _stack.Exit(); _indent = stepback; } private enum SerializeResult { NotUndefined, Undefined } private readonly struct PropertyEnumeration { private PropertyEnumeration(List keys, bool isEmpty) { Keys = keys; IsEmpty = isEmpty; } public static PropertyEnumeration FromList(List keys) => new PropertyEnumeration(keys, keys.Count == 0); public static PropertyEnumeration FromObjectInstance(ObjectInstance instance) { var allKeys = instance.GetOwnPropertyKeys(Types.String); RemoveUnserializableProperties(instance, allKeys); return new PropertyEnumeration(allKeys, allKeys.Count == 0); } private static void RemoveUnserializableProperties(ObjectInstance instance, List keys) { for (var i = 0; i < keys.Count; i++) { var key = keys[i]; var desc = instance.GetOwnProperty(key); if (desc == PropertyDescriptor.Undefined || !desc.Enumerable) { keys.RemoveAt(i); i--; } } } public readonly List Keys; public readonly bool IsEmpty; } } }