welsonjs/WelsonJS.Toolkit/WelsonJS.Launcher/JsSerializer.cs
Namhyeon, Go 130a6fd767 Add ChakraCore integration and native bootstrap logic
Introduces JsCore for ChakraCore P/Invoke, JsSerializer for JSON utilities via JS, and NativeBootstrap for robust native DLL loading. Updates Program.cs to initialize native dependencies at startup and registers new source files in the project file.
2025-09-26 17:04:14 +09:00

293 lines
11 KiB
C#

// JsSerializer.cs
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors
// https://github.com/gnh1201/welsonjs
//
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
namespace WelsonJS.Launcher
{
public sealed class JsSerializer : IDisposable
{
private readonly JsCore _core;
private readonly bool _ownsCore;
public JsSerializer() : this(new JsCore(), true) { }
public JsSerializer(JsCore core, bool ownsCore)
{
if (core == null) throw new ArgumentNullException("core");
_core = core;
_ownsCore = ownsCore;
}
public bool IsValid(string json)
{
if (json == null) throw new ArgumentNullException("json");
string script =
"(function(){try{JSON.parse(" + Q(json) + ");return '1';}catch(_){return '0';}})()";
string r = _core.EvaluateToString(script);
return r == "1";
}
public string Minify(string json)
{
if (json == null) throw new ArgumentNullException("json");
string script = "JSON.stringify(JSON.parse(" + Q(json) + "))";
return _core.EvaluateToString(script);
}
public string Pretty(string json, int space)
{
if (json == null) throw new ArgumentNullException("json");
space = Clamp(space, 0, 10);
string script = "JSON.stringify(JSON.parse(" + Q(json) + "),null," + space.ToString(CultureInfo.InvariantCulture) + ")";
return _core.EvaluateToString(script);
}
public string Normalize(string json)
{
return Minify(json);
}
/// <summary>
/// Extracts a value by a simple path of property names (numeric segment as string = array index).
/// Returns the selected value as a JSON string.
/// </summary>
public string Extract(string json, params string[] path)
{
if (path == null) path = new string[0];
object[] mixed = new object[path.Length];
for (int i = 0; i < path.Length; i++) mixed[i] = path[i];
return Extract(json, mixed);
}
/// <summary>
/// Extracts by a mixed path. Segments can be strings (object keys) or integers (array indices).
/// Returns the selected value as a JSON string (e.g., a JS string returns with quotes).
/// Usage: Extract(json, "items", 0, "name")
/// </summary>
public string Extract(string json, params object[] path)
{
if (json == null) throw new ArgumentNullException("json");
if (path == null) path = new object[0];
string jsPath = BuildJsPath(path);
var sb = new StringBuilder();
sb.Append("(function(){var v=JSON.parse(").Append(Q(json)).Append(");");
sb.Append("var p=").Append(jsPath).Append(";");
sb.Append("for(var i=0;i<p.length;i++){var k=p[i];");
sb.Append("if(Array.isArray(v) && typeof k==='number'){ v=v[k]; }");
sb.Append("else { v=(v==null?null:v[k]); }}");
sb.Append("return JSON.stringify(v);})()");
return _core.EvaluateToString(sb.ToString());
}
public string Serialize(object value, int space)
{
space = Clamp(space, 0, 10);
string expr = BuildJsExpression(value, new HashSet<object>(ReferenceEqualityComparer.Instance), 0);
string script = "JSON.stringify((" + expr + "),null," + space.ToString(CultureInfo.InvariantCulture) + ")";
return _core.EvaluateToString(script);
}
private static int Clamp(int v, int min, int max)
{
if (v < min) return min;
if (v > max) return max;
return v;
}
/// <summary>
/// Encode a .NET string as a JS double-quoted string literal.
/// </summary>
private static string Q(string s)
{
if (s == null) return "null";
var sb = new StringBuilder(s.Length + 16);
sb.Append('"');
for (int i = 0; i < s.Length; i++)
{
char ch = s[i];
switch (ch)
{
case '\\': sb.Append(@"\\"); break;
case '"': sb.Append("\\\""); break;
case '\b': sb.Append(@"\b"); break;
case '\f': sb.Append(@"\f"); break;
case '\n': sb.Append(@"\n"); break;
case '\r': sb.Append(@"\r"); break;
case '\t': sb.Append(@"\t"); break;
default:
if (char.IsControl(ch))
{
sb.Append("\\u");
sb.Append(((int)ch).ToString("X4"));
}
else
{
sb.Append(ch);
}
break;
}
}
sb.Append('"');
return sb.ToString();
}
/// <summary>
/// Builds a JS array literal representing the path.
/// Numeric segments are emitted as numbers; others as strings.
/// </summary>
private static string BuildJsPath(object[] segments)
{
if (segments == null || segments.Length == 0) return "[]";
var sb = new StringBuilder();
sb.Append('[');
for (int i = 0; i < segments.Length; i++)
{
if (i > 0) sb.Append(',');
object seg = segments[i];
// Treat integral types as numbers for array indexing
if (seg is sbyte || seg is byte ||
seg is short || seg is ushort ||
seg is int || seg is uint ||
seg is long || seg is ulong)
{
sb.Append(Convert.ToString(seg, CultureInfo.InvariantCulture));
}
else
{
string str = (seg == null) ? string.Empty : Convert.ToString(seg, CultureInfo.InvariantCulture);
sb.Append(Q(str));
}
}
sb.Append(']');
return sb.ToString();
}
private static bool IsNumeric(object v)
{
if (v == null) return false;
Type t = v.GetType();
t = Nullable.GetUnderlyingType(t) ?? t;
return t == typeof(byte) || t == typeof(sbyte) ||
t == typeof(short) || t == typeof(ushort) ||
t == typeof(int) || t == typeof(uint) ||
t == typeof(long) || t == typeof(ulong) ||
t == typeof(float) || t == typeof(double) ||
t == typeof(decimal);
}
private static bool IsImmutableLike(object v)
{
return v is string || v is bool ||
v is byte || v is sbyte ||
v is short || v is ushort ||
v is int || v is uint ||
v is long || v is ulong ||
v is float || v is double || v is decimal ||
v is Guid || v is DateTime || v is DateTimeOffset;
}
/// <summary>
/// Builds a safe JS expression for a .NET value (no engine calls here).
/// Engine will stringify the produced expression via JSON.stringify.
/// </summary>
private static string BuildJsExpression(object value, HashSet<object> seen, int depth)
{
if (depth > 64) return "null"; // depth guard
if (value == null) return "null";
// Primitives
if (value is string) return Q((string)value);
if (value is bool) return ((bool)value) ? "true" : "false";
if (IsNumeric(value))
return Convert.ToString(value, CultureInfo.InvariantCulture);
// Common value-like types → stringify as JS strings
if (value is Guid)
return Q(((Guid)value).ToString());
if (value is DateTime)
return Q(((DateTime)value).ToUniversalTime().ToString("o", CultureInfo.InvariantCulture));
if (value is DateTimeOffset)
return Q(((DateTimeOffset)value).ToUniversalTime().ToString("o", CultureInfo.InvariantCulture));
if (value is byte[])
return Q(Convert.ToBase64String((byte[])value));
// Prevent circular refs for reference types
if (!IsImmutableLike(value) && !seen.Add(value))
return "null";
// IDictionary (string keys only)
if (value is IDictionary)
{
var map = (IDictionary)value;
var sb = new StringBuilder();
sb.Append('{');
bool first = true;
foreach (DictionaryEntry kv in map)
{
string key = kv.Key as string;
if (key == null) continue; // JSON keys must be strings
if (!first) sb.Append(',');
first = false;
sb.Append(Q(key)).Append(':')
.Append(BuildJsExpression(kv.Value, seen, depth + 1));
}
sb.Append('}');
return sb.ToString();
}
// IEnumerable → array
if (value is IEnumerable)
{
var seq = (IEnumerable)value;
var sb = new StringBuilder();
sb.Append('[');
bool first = true;
foreach (object item in seq)
{
if (!first) sb.Append(',');
first = false;
sb.Append(BuildJsExpression(item, seen, depth + 1));
}
sb.Append(']');
return sb.ToString();
}
// Fallback → ToString() as JS string
string s = value.ToString();
return Q(s ?? string.Empty);
}
public void Dispose()
{
if (_ownsCore)
_core.Dispose();
}
/// <summary>
/// Reference equality comparer for cycle detection (works on .NET Framework).
/// </summary>
private sealed class ReferenceEqualityComparer : IEqualityComparer<object>
{
private ReferenceEqualityComparer() { }
public static readonly ReferenceEqualityComparer Instance = new ReferenceEqualityComparer();
bool IEqualityComparer<object>.Equals(object x, object y) { return object.ReferenceEquals(x, y); }
int IEqualityComparer<object>.GetHashCode(object obj)
{
return System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj);
}
}
}
}