chore(release): prepare public source release
This commit is contained in:
@@ -0,0 +1,872 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Pipes;
|
||||
using System.Reflection;
|
||||
using System.Security.AccessControl;
|
||||
using System.Security.Principal;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using BepInEx;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
namespace Mercury.UnityProbe
|
||||
{
|
||||
[BepInPlugin(MercuryUnityProbeBuild.PluginGuid, MercuryUnityProbeBuild.PluginName, MercuryUnityProbeBuild.PluginVersion)]
|
||||
public sealed class MercuryUnityProbePlugin : BaseUnityPlugin
|
||||
{
|
||||
private Thread pipeThread;
|
||||
private NamedPipeServerStream activeServer;
|
||||
private SynchronizationContext unityContext;
|
||||
private int mainThreadId;
|
||||
private volatile bool stopRequested;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
unityContext = SynchronizationContext.Current;
|
||||
mainThreadId = Thread.CurrentThread.ManagedThreadId;
|
||||
pipeThread = new Thread(PipeServerMain);
|
||||
pipeThread.Name = "MercuryUnityProbePipe";
|
||||
pipeThread.IsBackground = true;
|
||||
pipeThread.Start();
|
||||
Logger.LogInfo("Mercury Unity Probe pipe server thread started.");
|
||||
Logger.LogInfo("Mercury Unity Probe ready on pipe " + MercuryUnityProbeBuild.PipeName + ".");
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
Logger.LogInfo("Mercury Unity Probe BaseUnityPlugin OnDestroy fired; keeping pipe backend alive.");
|
||||
}
|
||||
|
||||
private void OnApplicationQuit()
|
||||
{
|
||||
stopRequested = true;
|
||||
Logger.LogInfo("Mercury Unity Probe stopping pipe backend for application quit.");
|
||||
TryDisposeActiveServer();
|
||||
}
|
||||
|
||||
private void PipeServerMain()
|
||||
{
|
||||
while (!stopRequested)
|
||||
{
|
||||
NamedPipeServerStream server = null;
|
||||
try
|
||||
{
|
||||
server = CreatePipeServer();
|
||||
activeServer = server;
|
||||
server.WaitForConnection();
|
||||
ServeConnection(server);
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
if (!stopRequested)
|
||||
{
|
||||
Logger.LogWarning("Named pipe server disposed unexpectedly.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (!stopRequested)
|
||||
{
|
||||
Logger.LogError("Named pipe server failed: " + ex);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
TryDisposeServer(server);
|
||||
|
||||
if (ReferenceEquals(activeServer, server))
|
||||
{
|
||||
activeServer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static NamedPipeServerStream CreatePipeServer()
|
||||
{
|
||||
PipeSecurity pipeSecurity = CreateCurrentUserPipeSecurity();
|
||||
return new NamedPipeServerStream(
|
||||
MercuryUnityProbeBuild.PipeName,
|
||||
PipeDirection.InOut,
|
||||
1,
|
||||
PipeTransmissionMode.Byte,
|
||||
PipeOptions.None,
|
||||
4096,
|
||||
4096,
|
||||
pipeSecurity);
|
||||
}
|
||||
|
||||
private static PipeSecurity CreateCurrentUserPipeSecurity()
|
||||
{
|
||||
WindowsIdentity identity = WindowsIdentity.GetCurrent();
|
||||
SecurityIdentifier user = identity == null ? null : identity.User;
|
||||
if (user == null)
|
||||
{
|
||||
throw new InvalidOperationException("Could not resolve the current Windows user for Mercury Unity Probe pipe security.");
|
||||
}
|
||||
|
||||
PipeSecurity pipeSecurity = new PipeSecurity();
|
||||
pipeSecurity.AddAccessRule(new PipeAccessRule(
|
||||
user,
|
||||
PipeAccessRights.ReadWrite | PipeAccessRights.CreateNewInstance,
|
||||
AccessControlType.Allow));
|
||||
return pipeSecurity;
|
||||
}
|
||||
|
||||
private void TryDisposeActiveServer()
|
||||
{
|
||||
TryDisposeServer(activeServer);
|
||||
}
|
||||
|
||||
private static void TryDisposeServer(NamedPipeServerStream server)
|
||||
{
|
||||
if (server == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
server.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private void ServeConnection(NamedPipeServerStream server)
|
||||
{
|
||||
StreamReader reader = null;
|
||||
StreamWriter writer = null;
|
||||
try
|
||||
{
|
||||
reader = new StreamReader(server, Encoding.UTF8, false, 4096, true);
|
||||
writer = new StreamWriter(server, new UTF8Encoding(false), 4096, true);
|
||||
writer.AutoFlush = true;
|
||||
string requestJson = ReadBoundedRequestLine(reader);
|
||||
Dictionary<string, object> response = HandleRequest(requestJson);
|
||||
writer.WriteLine(MercuryUnityProbeProtocol.Serialize(response));
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
if (writer != null)
|
||||
{
|
||||
Dictionary<string, object> response =
|
||||
MercuryUnityProbeProtocol.Error("invalid", ex.Message);
|
||||
writer.WriteLine(MercuryUnityProbeProtocol.Serialize(response));
|
||||
}
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
if (!IsBrokenPipe(ex))
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (writer != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
writer.Dispose();
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
if (!IsBrokenPipe(ex))
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (reader != null)
|
||||
{
|
||||
reader.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string ReadBoundedRequestLine(TextReader reader)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
while (true)
|
||||
{
|
||||
int value = reader.Read();
|
||||
if (value < 0)
|
||||
{
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
char current = (char)value;
|
||||
if (current == '\n')
|
||||
{
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
if (current == '\r')
|
||||
{
|
||||
if (reader.Peek() == '\n')
|
||||
{
|
||||
reader.Read();
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
if (builder.Length >= MercuryUnityProbeProtocol.MaxRequestChars)
|
||||
{
|
||||
throw new FormatException("JSON request exceeds the maximum request length.");
|
||||
}
|
||||
|
||||
builder.Append(current);
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<string, object> HandleRequest(string requestJson)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(requestJson))
|
||||
{
|
||||
return MercuryUnityProbeProtocol.Error("invalid", "Received an empty JSON request.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Dictionary<string, object> request =
|
||||
MercuryUnityProbeProtocol.DeserializeRequest(requestJson);
|
||||
if (request == null)
|
||||
{
|
||||
return MercuryUnityProbeProtocol.Error("invalid", "Failed to deserialize the JSON request.");
|
||||
}
|
||||
|
||||
string command = MercuryUnityProbeProtocol.ReadString(request, "command");
|
||||
if (string.IsNullOrWhiteSpace(command))
|
||||
{
|
||||
return MercuryUnityProbeProtocol.Error("invalid", "JSON request is missing the command field.");
|
||||
}
|
||||
|
||||
return InvokeOnUnityThread(delegate
|
||||
{
|
||||
return ExecuteCommand(command, request);
|
||||
}, command);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return MercuryUnityProbeProtocol.Error("invalid", ex.GetType().Name + ": " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsBrokenPipe(IOException error)
|
||||
{
|
||||
return error != null
|
||||
&& error.Message != null
|
||||
&& error.Message.IndexOf("pipe is broken", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
}
|
||||
|
||||
private Dictionary<string, object> InvokeOnUnityThread(Func<Dictionary<string, object>> action, string reason)
|
||||
{
|
||||
if (Thread.CurrentThread.ManagedThreadId == mainThreadId)
|
||||
{
|
||||
return action();
|
||||
}
|
||||
|
||||
if (unityContext != null)
|
||||
{
|
||||
Dictionary<string, object> result = null;
|
||||
Exception error = null;
|
||||
using (ManualResetEventSlim done = new ManualResetEventSlim(false))
|
||||
{
|
||||
unityContext.Post(delegate(object _)
|
||||
{
|
||||
try
|
||||
{
|
||||
result = action();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = ex;
|
||||
}
|
||||
finally
|
||||
{
|
||||
done.Set();
|
||||
}
|
||||
}, null);
|
||||
|
||||
if (!done.Wait(5000))
|
||||
{
|
||||
return MercuryUnityProbeProtocol.Error(reason, "Timed out waiting for the Unity SynchronizationContext.");
|
||||
}
|
||||
}
|
||||
|
||||
if (error != null)
|
||||
{
|
||||
return MercuryUnityProbeProtocol.Error(reason, error.GetType().Name + ": " + error.Message);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
if (ThreadingHelper.Instance == null)
|
||||
{
|
||||
return MercuryUnityProbeProtocol.Error(reason, "No Unity SynchronizationContext or ThreadingHelper is available.");
|
||||
}
|
||||
|
||||
Dictionary<string, object> fallbackResult = null;
|
||||
Exception fallbackError = null;
|
||||
using (ManualResetEventSlim fallbackDone = new ManualResetEventSlim(false))
|
||||
{
|
||||
ThreadingHelper.Instance.StartSyncInvoke(delegate
|
||||
{
|
||||
try
|
||||
{
|
||||
fallbackResult = action();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
fallbackError = ex;
|
||||
}
|
||||
finally
|
||||
{
|
||||
fallbackDone.Set();
|
||||
}
|
||||
});
|
||||
|
||||
if (!fallbackDone.Wait(5000))
|
||||
{
|
||||
return MercuryUnityProbeProtocol.Error(reason, "Timed out waiting for ThreadingHelper.");
|
||||
}
|
||||
}
|
||||
|
||||
if (fallbackError != null)
|
||||
{
|
||||
return MercuryUnityProbeProtocol.Error(reason, fallbackError.GetType().Name + ": " + fallbackError.Message);
|
||||
}
|
||||
|
||||
return fallbackResult;
|
||||
}
|
||||
|
||||
private Dictionary<string, object> ExecuteCommand(string command, IDictionary<string, object> request)
|
||||
{
|
||||
string normalized = command == null ? string.Empty : command.Trim().ToLowerInvariant();
|
||||
if (normalized == "status")
|
||||
{
|
||||
return BuildStatus();
|
||||
}
|
||||
|
||||
if (normalized == "scenes")
|
||||
{
|
||||
return BuildScenes();
|
||||
}
|
||||
|
||||
if (normalized == "find")
|
||||
{
|
||||
return BuildFind(MercuryUnityProbeProtocol.ReadString(request, "query"), MercuryUnityProbeProtocol.ReadLimit(request, 25));
|
||||
}
|
||||
|
||||
if (normalized == "inspect")
|
||||
{
|
||||
return BuildInspect(MercuryUnityProbeProtocol.ReadInt(request, "instance_id"));
|
||||
}
|
||||
|
||||
if (normalized == "static")
|
||||
{
|
||||
return BuildStatic(MercuryUnityProbeProtocol.ReadString(request, "type_name"));
|
||||
}
|
||||
|
||||
return MercuryUnityProbeProtocol.Error(command, "Unsupported command.");
|
||||
}
|
||||
|
||||
private Dictionary<string, object> BuildStatus()
|
||||
{
|
||||
List<string> scenes = new List<string>();
|
||||
int sceneCount = SceneManager.sceneCount;
|
||||
for (int i = 0; i < sceneCount; i++)
|
||||
{
|
||||
Scene scene = SceneManager.GetSceneAt(i);
|
||||
scenes.Add(scene.name ?? string.Empty);
|
||||
}
|
||||
|
||||
Dictionary<string, object> data = new Dictionary<string, object>(StringComparer.Ordinal);
|
||||
data["pipe_name"] = MercuryUnityProbeBuild.PipeName;
|
||||
data["plugin_version"] = MercuryUnityProbeBuild.PluginVersion;
|
||||
data["unity_version"] = Application.unityVersion ?? string.Empty;
|
||||
data["process_id"] = (uint)System.Diagnostics.Process.GetCurrentProcess().Id;
|
||||
data["scene_count"] = sceneCount;
|
||||
data["loaded_scene_names"] = scenes;
|
||||
return MercuryUnityProbeProtocol.Success("status", data);
|
||||
}
|
||||
|
||||
private Dictionary<string, object> BuildScenes()
|
||||
{
|
||||
List<Dictionary<string, object>> scenes = new List<Dictionary<string, object>>();
|
||||
int sceneCount = SceneManager.sceneCount;
|
||||
for (int i = 0; i < sceneCount; i++)
|
||||
{
|
||||
Scene scene = SceneManager.GetSceneAt(i);
|
||||
Dictionary<string, object> record = new Dictionary<string, object>(StringComparer.Ordinal);
|
||||
record["name"] = scene.name ?? string.Empty;
|
||||
record["path"] = scene.path ?? string.Empty;
|
||||
record["build_index"] = scene.buildIndex;
|
||||
record["loaded"] = scene.isLoaded;
|
||||
record["root_count"] = scene.rootCount;
|
||||
scenes.Add(record);
|
||||
}
|
||||
|
||||
Dictionary<string, object> data = new Dictionary<string, object>(StringComparer.Ordinal);
|
||||
data["scenes"] = scenes;
|
||||
return MercuryUnityProbeProtocol.Success("scenes", data);
|
||||
}
|
||||
|
||||
private Dictionary<string, object> BuildFind(string query, int limit)
|
||||
{
|
||||
string needle = query == null ? string.Empty : query.Trim();
|
||||
if (needle.Length == 0)
|
||||
{
|
||||
return MercuryUnityProbeProtocol.Error("find", "find requires a non-empty query.");
|
||||
}
|
||||
|
||||
List<Dictionary<string, object>> matches = new List<Dictionary<string, object>>();
|
||||
UnityEngine.Object[] objects = Resources.FindObjectsOfTypeAll<UnityEngine.Object>();
|
||||
for (int i = 0; i < objects.Length; i++)
|
||||
{
|
||||
UnityEngine.Object obj = objects[i];
|
||||
if (obj == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string typeName = obj.GetType().FullName ?? obj.GetType().Name;
|
||||
string objectName = obj.name ?? string.Empty;
|
||||
if (!ContainsIgnoreCase(typeName, needle) && !ContainsIgnoreCase(objectName, needle))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
matches.Add(DescribeObjectSummary(obj));
|
||||
if (matches.Count >= limit)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
matches.Sort(delegate(Dictionary<string, object> left, Dictionary<string, object> right)
|
||||
{
|
||||
return string.CompareOrdinal(
|
||||
Convert.ToString(left["type_name"]) + "|" + Convert.ToString(left["name"]),
|
||||
Convert.ToString(right["type_name"]) + "|" + Convert.ToString(right["name"]));
|
||||
});
|
||||
|
||||
Dictionary<string, object> data = new Dictionary<string, object>(StringComparer.Ordinal);
|
||||
data["query"] = needle;
|
||||
data["matches"] = matches;
|
||||
return MercuryUnityProbeProtocol.Success("find", data);
|
||||
}
|
||||
|
||||
private Dictionary<string, object> BuildInspect(int instanceId)
|
||||
{
|
||||
if (instanceId == 0)
|
||||
{
|
||||
return MercuryUnityProbeProtocol.Error("inspect", "inspect requires a non-zero instance id.");
|
||||
}
|
||||
|
||||
UnityEngine.Object obj = FindObjectByInstanceId(instanceId);
|
||||
if (obj == null)
|
||||
{
|
||||
return MercuryUnityProbeProtocol.Error("inspect", "No runtime object matched the requested instance id.");
|
||||
}
|
||||
|
||||
Dictionary<string, object> data = new Dictionary<string, object>(StringComparer.Ordinal);
|
||||
data["object"] = DescribeObjectSummary(obj);
|
||||
data["components"] = DescribeComponents(obj);
|
||||
data["fields"] = DescribeInstanceFields(obj);
|
||||
data["properties"] = DescribeInstanceProperties(obj);
|
||||
return MercuryUnityProbeProtocol.Success("inspect", data);
|
||||
}
|
||||
|
||||
private Dictionary<string, object> BuildStatic(string typeName)
|
||||
{
|
||||
string requested = typeName == null ? string.Empty : typeName.Trim();
|
||||
if (requested.Length == 0)
|
||||
{
|
||||
return MercuryUnityProbeProtocol.Error("static", "static requires a managed type name.");
|
||||
}
|
||||
|
||||
Type type = ResolveType(requested);
|
||||
if (type == null)
|
||||
{
|
||||
return MercuryUnityProbeProtocol.Error("static", "Could not resolve the requested managed type.");
|
||||
}
|
||||
|
||||
Dictionary<string, object> data = new Dictionary<string, object>(StringComparer.Ordinal);
|
||||
data["type_name"] = type.FullName ?? type.Name;
|
||||
data["assembly_name"] = type.Assembly.GetName().Name ?? string.Empty;
|
||||
data["fields"] = DescribeStaticFields(type);
|
||||
data["properties"] = DescribeStaticProperties(type);
|
||||
return MercuryUnityProbeProtocol.Success("static", data);
|
||||
}
|
||||
|
||||
private static bool ContainsIgnoreCase(string haystack, string needle)
|
||||
{
|
||||
if (string.IsNullOrEmpty(haystack) || string.IsNullOrEmpty(needle))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return haystack.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
}
|
||||
|
||||
private static UnityEngine.Object FindObjectByInstanceId(int instanceId)
|
||||
{
|
||||
UnityEngine.Object[] objects = Resources.FindObjectsOfTypeAll<UnityEngine.Object>();
|
||||
for (int i = 0; i < objects.Length; i++)
|
||||
{
|
||||
UnityEngine.Object obj = objects[i];
|
||||
if (obj != null && obj.GetInstanceID() == instanceId)
|
||||
{
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Dictionary<string, object> DescribeObjectSummary(UnityEngine.Object obj)
|
||||
{
|
||||
Dictionary<string, object> record = new Dictionary<string, object>(StringComparer.Ordinal);
|
||||
record["instance_id"] = obj.GetInstanceID();
|
||||
record["name"] = obj.name ?? string.Empty;
|
||||
record["type_name"] = obj.GetType().FullName ?? obj.GetType().Name;
|
||||
record["scene_name"] = GetSceneName(obj);
|
||||
record["hierarchy_path"] = GetHierarchyPath(obj);
|
||||
record["active"] = IsObjectActive(obj);
|
||||
return record;
|
||||
}
|
||||
|
||||
private static List<Dictionary<string, object>> DescribeComponents(UnityEngine.Object obj)
|
||||
{
|
||||
List<Dictionary<string, object>> items = new List<Dictionary<string, object>>();
|
||||
GameObject gameObject = null;
|
||||
if (obj is GameObject)
|
||||
{
|
||||
gameObject = (GameObject)obj;
|
||||
}
|
||||
else if (obj is Component)
|
||||
{
|
||||
gameObject = ((Component)obj).gameObject;
|
||||
}
|
||||
|
||||
if (gameObject == null)
|
||||
{
|
||||
return items;
|
||||
}
|
||||
|
||||
Component[] components = gameObject.GetComponents<Component>();
|
||||
for (int i = 0; i < components.Length; i++)
|
||||
{
|
||||
Component component = components[i];
|
||||
if (component == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Dictionary<string, object> record = new Dictionary<string, object>(StringComparer.Ordinal);
|
||||
record["instance_id"] = component.GetInstanceID();
|
||||
record["type_name"] = component.GetType().FullName ?? component.GetType().Name;
|
||||
items.Add(record);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private static List<Dictionary<string, object>> DescribeInstanceFields(object target)
|
||||
{
|
||||
return DescribeMembers(
|
||||
target.GetType().GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance),
|
||||
24,
|
||||
delegate(FieldInfo field)
|
||||
{
|
||||
object value = null;
|
||||
try
|
||||
{
|
||||
value = field.GetValue(target);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return CreateValueMember(field.Name, field.FieldType, value);
|
||||
});
|
||||
}
|
||||
|
||||
private static List<Dictionary<string, object>> DescribeInstanceProperties(object target)
|
||||
{
|
||||
return DescribeMembers(
|
||||
target.GetType().GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance),
|
||||
24,
|
||||
delegate(PropertyInfo property)
|
||||
{
|
||||
if (property.GetIndexParameters().Length != 0 || property.GetGetMethod(true) == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
object value = null;
|
||||
try
|
||||
{
|
||||
value = property.GetValue(target, null);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return CreateValueMember(property.Name, property.PropertyType, value);
|
||||
});
|
||||
}
|
||||
|
||||
private static List<Dictionary<string, object>> DescribeStaticFields(Type type)
|
||||
{
|
||||
return DescribeMembers(
|
||||
type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static),
|
||||
32,
|
||||
delegate(FieldInfo field)
|
||||
{
|
||||
object value = null;
|
||||
try
|
||||
{
|
||||
value = field.GetValue(null);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return CreateValueMember(field.Name, field.FieldType, value);
|
||||
});
|
||||
}
|
||||
|
||||
private static List<Dictionary<string, object>> DescribeStaticProperties(Type type)
|
||||
{
|
||||
return DescribeMembers(
|
||||
type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static),
|
||||
32,
|
||||
delegate(PropertyInfo property)
|
||||
{
|
||||
if (property.GetIndexParameters().Length != 0 || property.GetGetMethod(true) == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
object value = null;
|
||||
try
|
||||
{
|
||||
value = property.GetValue(null, null);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return CreateValueMember(property.Name, property.PropertyType, value);
|
||||
});
|
||||
}
|
||||
|
||||
private delegate Dictionary<string, object> MemberFormatter<TMember>(TMember member);
|
||||
|
||||
private static List<Dictionary<string, object>> DescribeMembers<TMember>(TMember[] members, int limit, MemberFormatter<TMember> formatter)
|
||||
{
|
||||
List<Dictionary<string, object>> items = new List<Dictionary<string, object>>();
|
||||
Array.Sort(members, delegate(TMember left, TMember right)
|
||||
{
|
||||
string leftName = GetMemberName(left);
|
||||
string rightName = GetMemberName(right);
|
||||
return string.CompareOrdinal(leftName, rightName);
|
||||
});
|
||||
|
||||
for (int i = 0; i < members.Length && items.Count < limit; i++)
|
||||
{
|
||||
Dictionary<string, object> record = formatter(members[i]);
|
||||
if (record != null)
|
||||
{
|
||||
items.Add(record);
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private static string GetMemberName<TMember>(TMember member)
|
||||
{
|
||||
MemberInfo info = member as MemberInfo;
|
||||
return info == null ? string.Empty : info.Name ?? string.Empty;
|
||||
}
|
||||
|
||||
private static Dictionary<string, object> CreateValueMember(string name, Type declaredType, object value)
|
||||
{
|
||||
Dictionary<string, object> record = new Dictionary<string, object>(StringComparer.Ordinal);
|
||||
record["name"] = name ?? string.Empty;
|
||||
record["declared_type"] = declaredType == null ? string.Empty : (declaredType.FullName ?? declaredType.Name ?? string.Empty);
|
||||
record["value"] = FormatValue(value);
|
||||
return record;
|
||||
}
|
||||
|
||||
private static string FormatValue(object value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return "null";
|
||||
}
|
||||
|
||||
if (value is string)
|
||||
{
|
||||
return TrimValue((string)value);
|
||||
}
|
||||
|
||||
if (value is bool || value is byte || value is sbyte || value is short || value is ushort ||
|
||||
value is int || value is uint || value is long || value is ulong || value is float ||
|
||||
value is double || value is decimal || value is char)
|
||||
{
|
||||
return Convert.ToString(value) ?? string.Empty;
|
||||
}
|
||||
|
||||
if (value is Enum)
|
||||
{
|
||||
return value.GetType().Name + "." + value;
|
||||
}
|
||||
|
||||
if (value is UnityEngine.Object)
|
||||
{
|
||||
UnityEngine.Object obj = (UnityEngine.Object)value;
|
||||
return (obj.GetType().FullName ?? obj.GetType().Name) + "#" + obj.GetInstanceID() + ":" + (obj.name ?? string.Empty);
|
||||
}
|
||||
|
||||
if (value is IList)
|
||||
{
|
||||
IList list = (IList)value;
|
||||
return "list[count=" + list.Count + "]";
|
||||
}
|
||||
|
||||
if (value is IEnumerable)
|
||||
{
|
||||
return "enumerable";
|
||||
}
|
||||
|
||||
return TrimValue(Convert.ToString(value) ?? value.GetType().Name);
|
||||
}
|
||||
|
||||
private static string TrimValue(string text)
|
||||
{
|
||||
if (text == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
string trimmed = text.Replace("\r", "\\r").Replace("\n", "\\n");
|
||||
return trimmed.Length <= 160 ? trimmed : trimmed.Substring(0, 157) + "...";
|
||||
}
|
||||
|
||||
private static string GetSceneName(UnityEngine.Object obj)
|
||||
{
|
||||
if (obj is GameObject)
|
||||
{
|
||||
return ((GameObject)obj).scene.name ?? string.Empty;
|
||||
}
|
||||
|
||||
if (obj is Component)
|
||||
{
|
||||
Component component = (Component)obj;
|
||||
return component.gameObject == null ? string.Empty : (component.gameObject.scene.name ?? string.Empty);
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static string GetHierarchyPath(UnityEngine.Object obj)
|
||||
{
|
||||
Transform transform = null;
|
||||
if (obj is GameObject)
|
||||
{
|
||||
transform = ((GameObject)obj).transform;
|
||||
}
|
||||
else if (obj is Component)
|
||||
{
|
||||
transform = ((Component)obj).transform;
|
||||
}
|
||||
|
||||
if (transform == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
List<string> parts = new List<string>();
|
||||
while (transform != null)
|
||||
{
|
||||
parts.Add(transform.name ?? string.Empty);
|
||||
transform = transform.parent;
|
||||
}
|
||||
|
||||
parts.Reverse();
|
||||
return "/" + string.Join("/", parts.ToArray());
|
||||
}
|
||||
|
||||
private static bool IsObjectActive(UnityEngine.Object obj)
|
||||
{
|
||||
if (obj is Behaviour)
|
||||
{
|
||||
return ((Behaviour)obj).isActiveAndEnabled;
|
||||
}
|
||||
|
||||
if (obj is Component)
|
||||
{
|
||||
Component component = (Component)obj;
|
||||
return component.gameObject != null && component.gameObject.activeInHierarchy;
|
||||
}
|
||||
|
||||
if (obj is GameObject)
|
||||
{
|
||||
return ((GameObject)obj).activeInHierarchy;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Type ResolveType(string requested)
|
||||
{
|
||||
Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
|
||||
for (int i = 0; i < assemblies.Length; i++)
|
||||
{
|
||||
Type type = assemblies[i].GetType(requested, false);
|
||||
if (type != null)
|
||||
{
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < assemblies.Length; i++)
|
||||
{
|
||||
Type[] types;
|
||||
try
|
||||
{
|
||||
types = assemblies[i].GetTypes();
|
||||
}
|
||||
catch
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int j = 0; j < types.Length; j++)
|
||||
{
|
||||
Type type = types[j];
|
||||
string fullName = type.FullName ?? string.Empty;
|
||||
string name = type.Name ?? string.Empty;
|
||||
if (string.Equals(fullName, requested, StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(name, requested, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,615 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace Mercury.UnityProbe
|
||||
{
|
||||
internal static class MercuryUnityProbeProtocol
|
||||
{
|
||||
internal const int MaxRequestChars = 64 * 1024;
|
||||
private const int MaxJsonDepth = 64;
|
||||
private const int MaxJsonNodes = 4096;
|
||||
|
||||
internal static string Serialize(object value)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
WriteJsonValue(builder, value);
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
internal static Dictionary<string, object> DeserializeRequest(string json)
|
||||
{
|
||||
if (json != null && json.Length > MaxRequestChars)
|
||||
{
|
||||
throw new FormatException("JSON request exceeds the maximum request length.");
|
||||
}
|
||||
|
||||
JsonParser parser = new JsonParser(json ?? string.Empty);
|
||||
object value = parser.ParseValue();
|
||||
parser.SkipWhitespace();
|
||||
if (!parser.IsComplete)
|
||||
{
|
||||
throw new FormatException("Trailing characters after the JSON request.");
|
||||
}
|
||||
|
||||
Dictionary<string, object> request = value as Dictionary<string, object>;
|
||||
if (request == null)
|
||||
{
|
||||
throw new FormatException("JSON request root must be an object.");
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
internal static Dictionary<string, object> Success(string kind, object data)
|
||||
{
|
||||
Dictionary<string, object> response = new Dictionary<string, object>(StringComparer.Ordinal);
|
||||
response["ok"] = true;
|
||||
response["kind"] = kind ?? "";
|
||||
response["data"] = data;
|
||||
response["error"] = null;
|
||||
return response;
|
||||
}
|
||||
|
||||
internal static Dictionary<string, object> Error(string kind, string error)
|
||||
{
|
||||
Dictionary<string, object> response = new Dictionary<string, object>(StringComparer.Ordinal);
|
||||
response["ok"] = false;
|
||||
response["kind"] = kind ?? "";
|
||||
response["data"] = null;
|
||||
response["error"] = error ?? "Unknown bridge error.";
|
||||
return response;
|
||||
}
|
||||
|
||||
internal static string ReadString(IDictionary<string, object> request, string key)
|
||||
{
|
||||
if (request == null || key == null || !request.ContainsKey(key))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
object raw = request[key];
|
||||
return raw == null ? string.Empty : Convert.ToString(raw, CultureInfo.InvariantCulture) ?? string.Empty;
|
||||
}
|
||||
|
||||
internal static int ReadInt(IDictionary<string, object> request, string key)
|
||||
{
|
||||
if (request == null || key == null || !request.ContainsKey(key) || request[key] == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return Convert.ToInt32(request[key], CultureInfo.InvariantCulture);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
internal static int ReadLimit(IDictionary<string, object> request, int fallback)
|
||||
{
|
||||
int value = ReadInt(request, "limit");
|
||||
if (value <= 0)
|
||||
{
|
||||
return fallback;
|
||||
}
|
||||
|
||||
if (value > 250)
|
||||
{
|
||||
return 250;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private static void WriteJsonValue(StringBuilder builder, object value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
builder.Append("null");
|
||||
return;
|
||||
}
|
||||
|
||||
if (value is string)
|
||||
{
|
||||
string text = (string)value;
|
||||
WriteJsonString(builder, text);
|
||||
return;
|
||||
}
|
||||
|
||||
if (value is char)
|
||||
{
|
||||
char character = (char)value;
|
||||
WriteJsonString(builder, character.ToString());
|
||||
return;
|
||||
}
|
||||
|
||||
if (value is bool)
|
||||
{
|
||||
bool boolean = (bool)value;
|
||||
builder.Append(boolean ? "true" : "false");
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsNumericValue(value))
|
||||
{
|
||||
builder.Append(Convert.ToString(value, CultureInfo.InvariantCulture));
|
||||
return;
|
||||
}
|
||||
|
||||
if (value is IDictionary<string, object>)
|
||||
{
|
||||
IDictionary<string, object> objectDictionary = (IDictionary<string, object>)value;
|
||||
WriteJsonObject(builder, objectDictionary);
|
||||
return;
|
||||
}
|
||||
|
||||
if (value is IDictionary)
|
||||
{
|
||||
IDictionary dictionary = (IDictionary)value;
|
||||
WriteJsonDictionary(builder, dictionary);
|
||||
return;
|
||||
}
|
||||
|
||||
if (value is IEnumerable)
|
||||
{
|
||||
IEnumerable enumerable = (IEnumerable)value;
|
||||
WriteJsonArray(builder, enumerable);
|
||||
return;
|
||||
}
|
||||
|
||||
if (value is Enum)
|
||||
{
|
||||
Enum enumValue = (Enum)value;
|
||||
WriteJsonString(builder, enumValue.ToString());
|
||||
return;
|
||||
}
|
||||
|
||||
WriteJsonString(
|
||||
builder,
|
||||
Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty
|
||||
);
|
||||
}
|
||||
|
||||
private static bool IsNumericValue(object value)
|
||||
{
|
||||
switch (Type.GetTypeCode(value.GetType()))
|
||||
{
|
||||
case TypeCode.Byte:
|
||||
case TypeCode.Decimal:
|
||||
case TypeCode.Double:
|
||||
case TypeCode.Int16:
|
||||
case TypeCode.Int32:
|
||||
case TypeCode.Int64:
|
||||
case TypeCode.SByte:
|
||||
case TypeCode.Single:
|
||||
case TypeCode.UInt16:
|
||||
case TypeCode.UInt32:
|
||||
case TypeCode.UInt64:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteJsonObject(StringBuilder builder, IDictionary<string, object> dictionary)
|
||||
{
|
||||
builder.Append('{');
|
||||
bool wroteAny = false;
|
||||
foreach (KeyValuePair<string, object> entry in dictionary)
|
||||
{
|
||||
if (wroteAny)
|
||||
{
|
||||
builder.Append(',');
|
||||
}
|
||||
|
||||
WriteJsonString(builder, entry.Key ?? string.Empty);
|
||||
builder.Append(':');
|
||||
WriteJsonValue(builder, entry.Value);
|
||||
wroteAny = true;
|
||||
}
|
||||
|
||||
builder.Append('}');
|
||||
}
|
||||
|
||||
private static void WriteJsonDictionary(StringBuilder builder, IDictionary dictionary)
|
||||
{
|
||||
builder.Append('{');
|
||||
bool wroteAny = false;
|
||||
foreach (DictionaryEntry entry in dictionary)
|
||||
{
|
||||
if (wroteAny)
|
||||
{
|
||||
builder.Append(',');
|
||||
}
|
||||
|
||||
WriteJsonString(
|
||||
builder,
|
||||
Convert.ToString(entry.Key, CultureInfo.InvariantCulture) ?? string.Empty
|
||||
);
|
||||
builder.Append(':');
|
||||
WriteJsonValue(builder, entry.Value);
|
||||
wroteAny = true;
|
||||
}
|
||||
|
||||
builder.Append('}');
|
||||
}
|
||||
|
||||
private static void WriteJsonArray(StringBuilder builder, IEnumerable values)
|
||||
{
|
||||
builder.Append('[');
|
||||
bool wroteAny = false;
|
||||
foreach (object value in values)
|
||||
{
|
||||
if (wroteAny)
|
||||
{
|
||||
builder.Append(',');
|
||||
}
|
||||
|
||||
WriteJsonValue(builder, value);
|
||||
wroteAny = true;
|
||||
}
|
||||
|
||||
builder.Append(']');
|
||||
}
|
||||
|
||||
private static void WriteJsonString(StringBuilder builder, string value)
|
||||
{
|
||||
builder.Append('"');
|
||||
for (int index = 0; index < value.Length; index++)
|
||||
{
|
||||
char character = value[index];
|
||||
switch (character)
|
||||
{
|
||||
case '"':
|
||||
builder.Append("\\\"");
|
||||
break;
|
||||
case '\\':
|
||||
builder.Append("\\\\");
|
||||
break;
|
||||
case '\b':
|
||||
builder.Append("\\b");
|
||||
break;
|
||||
case '\f':
|
||||
builder.Append("\\f");
|
||||
break;
|
||||
case '\n':
|
||||
builder.Append("\\n");
|
||||
break;
|
||||
case '\r':
|
||||
builder.Append("\\r");
|
||||
break;
|
||||
case '\t':
|
||||
builder.Append("\\t");
|
||||
break;
|
||||
default:
|
||||
if (character < 0x20)
|
||||
{
|
||||
builder.Append("\\u");
|
||||
builder.Append(((int)character).ToString("x4", CultureInfo.InvariantCulture));
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append(character);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
builder.Append('"');
|
||||
}
|
||||
|
||||
private sealed class JsonParser
|
||||
{
|
||||
private readonly string text;
|
||||
private int index;
|
||||
private int depth;
|
||||
private int nodes;
|
||||
|
||||
internal JsonParser(string text)
|
||||
{
|
||||
this.text = text ?? string.Empty;
|
||||
index = 0;
|
||||
}
|
||||
|
||||
internal bool IsComplete
|
||||
{
|
||||
get { return index >= text.Length; }
|
||||
}
|
||||
|
||||
internal void SkipWhitespace()
|
||||
{
|
||||
while (index < text.Length && char.IsWhiteSpace(text[index]))
|
||||
{
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
internal object ParseValue()
|
||||
{
|
||||
CountNode();
|
||||
SkipWhitespace();
|
||||
if (index >= text.Length)
|
||||
{
|
||||
throw new FormatException("Unexpected end of JSON input.");
|
||||
}
|
||||
|
||||
char current = text[index];
|
||||
switch (current)
|
||||
{
|
||||
case '{':
|
||||
return ParseObject();
|
||||
case '[':
|
||||
return ParseArray();
|
||||
case '"':
|
||||
return ParseString();
|
||||
case 't':
|
||||
ParseLiteral("true");
|
||||
return true;
|
||||
case 'f':
|
||||
ParseLiteral("false");
|
||||
return false;
|
||||
case 'n':
|
||||
ParseLiteral("null");
|
||||
return null;
|
||||
default:
|
||||
if (current == '-' || char.IsDigit(current))
|
||||
{
|
||||
return ParseNumber();
|
||||
}
|
||||
|
||||
throw new FormatException("Unsupported JSON token at position " + index + ".");
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<string, object> ParseObject()
|
||||
{
|
||||
Expect('{');
|
||||
EnterContainer();
|
||||
try
|
||||
{
|
||||
Dictionary<string, object> dictionary = new Dictionary<string, object>(StringComparer.Ordinal);
|
||||
SkipWhitespace();
|
||||
if (TryConsume('}'))
|
||||
{
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
while (true)
|
||||
{
|
||||
SkipWhitespace();
|
||||
string key = ParseString();
|
||||
SkipWhitespace();
|
||||
Expect(':');
|
||||
object value = ParseValue();
|
||||
dictionary[key] = value;
|
||||
SkipWhitespace();
|
||||
if (TryConsume('}'))
|
||||
{
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
Expect(',');
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
LeaveContainer();
|
||||
}
|
||||
}
|
||||
|
||||
private List<object> ParseArray()
|
||||
{
|
||||
Expect('[');
|
||||
EnterContainer();
|
||||
try
|
||||
{
|
||||
List<object> list = new List<object>();
|
||||
SkipWhitespace();
|
||||
if (TryConsume(']'))
|
||||
{
|
||||
return list;
|
||||
}
|
||||
|
||||
while (true)
|
||||
{
|
||||
list.Add(ParseValue());
|
||||
SkipWhitespace();
|
||||
if (TryConsume(']'))
|
||||
{
|
||||
return list;
|
||||
}
|
||||
|
||||
Expect(',');
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
LeaveContainer();
|
||||
}
|
||||
}
|
||||
|
||||
private void CountNode()
|
||||
{
|
||||
nodes++;
|
||||
if (nodes > MaxJsonNodes)
|
||||
{
|
||||
throw new FormatException("JSON request exceeds the maximum node count.");
|
||||
}
|
||||
}
|
||||
|
||||
private void EnterContainer()
|
||||
{
|
||||
depth++;
|
||||
if (depth > MaxJsonDepth)
|
||||
{
|
||||
throw new FormatException("JSON request exceeds the maximum nesting depth.");
|
||||
}
|
||||
}
|
||||
|
||||
private void LeaveContainer()
|
||||
{
|
||||
depth--;
|
||||
}
|
||||
|
||||
private string ParseString()
|
||||
{
|
||||
Expect('"');
|
||||
StringBuilder builder = new StringBuilder();
|
||||
while (index < text.Length)
|
||||
{
|
||||
char current = text[index++];
|
||||
if (current == '"')
|
||||
{
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
if (current != '\\')
|
||||
{
|
||||
builder.Append(current);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (index >= text.Length)
|
||||
{
|
||||
throw new FormatException("Unterminated escape sequence in JSON string.");
|
||||
}
|
||||
|
||||
char escaped = text[index++];
|
||||
switch (escaped)
|
||||
{
|
||||
case '"':
|
||||
builder.Append('"');
|
||||
break;
|
||||
case '\\':
|
||||
builder.Append('\\');
|
||||
break;
|
||||
case '/':
|
||||
builder.Append('/');
|
||||
break;
|
||||
case 'b':
|
||||
builder.Append('\b');
|
||||
break;
|
||||
case 'f':
|
||||
builder.Append('\f');
|
||||
break;
|
||||
case 'n':
|
||||
builder.Append('\n');
|
||||
break;
|
||||
case 'r':
|
||||
builder.Append('\r');
|
||||
break;
|
||||
case 't':
|
||||
builder.Append('\t');
|
||||
break;
|
||||
case 'u':
|
||||
builder.Append(ParseUnicodeEscape());
|
||||
break;
|
||||
default:
|
||||
throw new FormatException("Unsupported escape sequence \\" + escaped + " in JSON string.");
|
||||
}
|
||||
}
|
||||
|
||||
throw new FormatException("Unterminated JSON string.");
|
||||
}
|
||||
|
||||
private char ParseUnicodeEscape()
|
||||
{
|
||||
if (index + 4 > text.Length)
|
||||
{
|
||||
throw new FormatException("Incomplete unicode escape in JSON string.");
|
||||
}
|
||||
|
||||
string hex = text.Substring(index, 4);
|
||||
index += 4;
|
||||
return (char)int.Parse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private object ParseNumber()
|
||||
{
|
||||
int start = index;
|
||||
if (text[index] == '-')
|
||||
{
|
||||
index++;
|
||||
}
|
||||
|
||||
while (index < text.Length && char.IsDigit(text[index]))
|
||||
{
|
||||
index++;
|
||||
}
|
||||
|
||||
if (index < text.Length && text[index] == '.')
|
||||
{
|
||||
index++;
|
||||
while (index < text.Length && char.IsDigit(text[index]))
|
||||
{
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
if (index < text.Length && (text[index] == 'e' || text[index] == 'E'))
|
||||
{
|
||||
index++;
|
||||
if (index < text.Length && (text[index] == '+' || text[index] == '-'))
|
||||
{
|
||||
index++;
|
||||
}
|
||||
|
||||
while (index < text.Length && char.IsDigit(text[index]))
|
||||
{
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
string token = text.Substring(start, index - start);
|
||||
if (token.IndexOfAny(new[] { '.', 'e', 'E' }) >= 0)
|
||||
{
|
||||
return double.Parse(token, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
return long.Parse(token, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private void ParseLiteral(string literal)
|
||||
{
|
||||
for (int offset = 0; offset < literal.Length; offset++)
|
||||
{
|
||||
if (index + offset >= text.Length || text[index + offset] != literal[offset])
|
||||
{
|
||||
throw new FormatException("Invalid JSON literal at position " + index + ".");
|
||||
}
|
||||
}
|
||||
|
||||
index += literal.Length;
|
||||
}
|
||||
|
||||
private void Expect(char expected)
|
||||
{
|
||||
SkipWhitespace();
|
||||
if (index >= text.Length || text[index] != expected)
|
||||
{
|
||||
throw new FormatException("Expected '" + expected + "' at position " + index + ".");
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
private bool TryConsume(char value)
|
||||
{
|
||||
SkipWhitespace();
|
||||
if (index < text.Length && text[index] == value)
|
||||
{
|
||||
index++;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user