616 lines
19 KiB
C#
616 lines
19 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|
|
}
|