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 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 request = value as Dictionary; if (request == null) { throw new FormatException("JSON request root must be an object."); } return request; } internal static Dictionary Success(string kind, object data) { Dictionary response = new Dictionary(StringComparer.Ordinal); response["ok"] = true; response["kind"] = kind ?? ""; response["data"] = data; response["error"] = null; return response; } internal static Dictionary Error(string kind, string error) { Dictionary response = new Dictionary(StringComparer.Ordinal); response["ok"] = false; response["kind"] = kind ?? ""; response["data"] = null; response["error"] = error ?? "Unknown bridge error."; return response; } internal static string ReadString(IDictionary 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 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 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) { IDictionary objectDictionary = (IDictionary)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 dictionary) { builder.Append('{'); bool wroteAny = false; foreach (KeyValuePair 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 ParseObject() { Expect('{'); EnterContainer(); try { Dictionary dictionary = new Dictionary(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 ParseArray() { Expect('['); EnterContainer(); try { List list = new List(); 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; } } } }