using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; using System.Text.RegularExpressions; namespace UnityEngine.InputSystem.Utilities { /// /// A JSON parser that instead of turning a string in JSON format into a /// C# object graph, allows navigating the source text directly. /// /// /// This helper is most useful for avoiding a great many string and general object allocations /// that would happen when turning a JSON object into a C# object graph. /// internal struct JsonParser { public JsonParser(string json) : this() { if (json == null) throw new ArgumentNullException(nameof(json)); m_Text = json; m_Length = json.Length; } public void Reset() { m_Position = 0; m_MatchAnyElementInArray = false; m_DryRun = false; } public override string ToString() { if (m_Text != null) return $"{m_Position}: {m_Text.Substring(m_Position)}"; return base.ToString(); } /// /// Navigate to the given property. /// /// /// /// This navigates from the current property. /// public bool NavigateToProperty(string path) { if (string.IsNullOrEmpty(path)) throw new ArgumentNullException(nameof(path)); var pathLength = path.Length; var pathPosition = 0; m_DryRun = true; if (!ParseToken('{')) return false; while (m_Position < m_Length && pathPosition < pathLength) { // Find start of property name. SkipWhitespace(); if (m_Position == m_Length) return false; if (m_Text[m_Position] != '"') return false; ++m_Position; // Try to match single path component. var pathStartPosition = pathPosition; while (pathPosition < pathLength) { var ch = path[pathPosition]; if (ch == '/' || ch == '[') break; if (m_Text[m_Position] != ch) break; ++m_Position; ++pathPosition; } // See if we have a match. if (m_Position < m_Length && m_Text[m_Position] == '"' && (pathPosition >= pathLength || path[pathPosition] == '/' || path[pathPosition] == '[')) { // Have matched a property name. Navigate to value. ++m_Position; if (!SkipToValue()) return false; // Check if we have matched everything in the path. if (pathPosition >= pathLength) return true; if (path[pathPosition] == '/') { ++pathPosition; if (!ParseToken('{')) return false; } else if (path[pathPosition] == '[') { ++pathPosition; if (pathPosition == pathLength) throw new ArgumentException("Malformed JSON property path: " + path, nameof(path)); if (path[pathPosition] == ']') { m_MatchAnyElementInArray = true; ++pathPosition; if (pathPosition == pathLength) return true; } else throw new NotImplementedException("Navigating to specific array element"); } } else { // This property isn't it. Skip the property and its value and reset // to where we started in this iteration in the property path. pathPosition = pathStartPosition; while (m_Position < m_Length && m_Text[m_Position] != '"') ++m_Position; if (m_Position == m_Length || m_Text[m_Position] != '"') return false; ++m_Position; if (!SkipToValue() || !ParseValue()) return false; SkipWhitespace(); if (m_Position == m_Length || m_Text[m_Position] == '}' || m_Text[m_Position] != ',') return false; ++m_Position; } } return false; } /// /// Return true if the current property has a value matching . /// /// /// public bool CurrentPropertyHasValueEqualTo(JsonValue expectedValue) { // Grab property value. var savedPosition = m_Position; m_DryRun = false; if (!ParseValue(out var propertyValue)) { m_Position = savedPosition; return false; } m_Position = savedPosition; // Match given value. var isMatch = false; if (propertyValue.type == JsonValueType.Array && m_MatchAnyElementInArray) { var array = propertyValue.arrayValue; for (var i = 0; !isMatch && i < array.Count; ++i) isMatch = array[i] == expectedValue; } else { isMatch = propertyValue == expectedValue; } return isMatch; } public bool ParseToken(char token) { SkipWhitespace(); if (m_Position == m_Length) return false; if (m_Text[m_Position] != token) return false; ++m_Position; SkipWhitespace(); return m_Position < m_Length; } public bool ParseValue() { return ParseValue(out var result); } public bool ParseValue(out JsonValue result) { result = default; SkipWhitespace(); if (m_Position == m_Length) return false; var ch = m_Text[m_Position]; switch (ch) { case '"': if (ParseStringValue(out result)) return true; break; case '[': if (ParseArrayValue(out result)) return true; break; case '{': if (ParseObjectValue(out result)) return true; break; case 't': case 'f': if (ParseBooleanValue(out result)) return true; break; case 'n': if (ParseNullValue(out result)) return true; break; default: if (ParseNumber(out result)) return true; break; } return false; } public bool ParseStringValue(out JsonValue result) { result = default; SkipWhitespace(); if (m_Position == m_Length || m_Text[m_Position] != '"') return false; ++m_Position; var startIndex = m_Position; var hasEscapes = false; while (m_Position < m_Length) { var ch = m_Text[m_Position]; if (ch == '\\') { ++m_Position; if (m_Position == m_Length) break; hasEscapes = true; } else if (ch == '"') { ++m_Position; result = new JsonString { text = new Substring(m_Text, startIndex, m_Position - startIndex - 1), hasEscapes = hasEscapes }; return true; } ++m_Position; } return false; } public bool ParseArrayValue(out JsonValue result) { result = default; SkipWhitespace(); if (m_Position == m_Length || m_Text[m_Position] != '[') return false; ++m_Position; if (m_Position == m_Length) return false; if (m_Text[m_Position] == ']') { // Empty array. result = new JsonValue { type = JsonValueType.Array }; ++m_Position; return true; } List values = null; if (!m_DryRun) values = new List(); while (m_Position < m_Length) { if (!ParseValue(out var value)) return false; if (!m_DryRun) values.Add(value); SkipWhitespace(); if (m_Position == m_Length) return false; var ch = m_Text[m_Position]; if (ch == ']') { ++m_Position; if (!m_DryRun) result = values; return true; } if (ch == ',') ++m_Position; } return false; } public bool ParseObjectValue(out JsonValue result) { result = default; if (!ParseToken('{')) return false; if (m_Position < m_Length && m_Text[m_Position] == '}') { result = new JsonValue { type = JsonValueType.Object }; ++m_Position; return true; } while (m_Position < m_Length) { if (!ParseStringValue(out var propertyName)) return false; if (!SkipToValue()) return false; if (!ParseValue(out var propertyValue)) return false; if (!m_DryRun) throw new NotImplementedException(); SkipWhitespace(); if (m_Position < m_Length && m_Text[m_Position] == '}') { if (!m_DryRun) throw new NotImplementedException(); ++m_Position; return true; } } return false; } public bool ParseNumber(out JsonValue result) { result = default; SkipWhitespace(); if (m_Position == m_Length) return false; var negative = false; var haveFractionalPart = false; var integralPart = 0L; var fractionalPart = 0.0; var fractionalDivisor = 10.0; var exponent = 0; // Parse sign. if (m_Text[m_Position] == '-') { negative = true; ++m_Position; } if (m_Position == m_Length || !char.IsDigit(m_Text[m_Position])) return false; // Parse integral part. while (m_Position < m_Length) { var ch = m_Text[m_Position]; if (ch == '.') break; if (ch < '0' || ch > '9') break; integralPart = integralPart * 10 + ch - '0'; ++m_Position; } // Parse fractional part. if (m_Position < m_Length && m_Text[m_Position] == '.') { haveFractionalPart = true; ++m_Position; if (m_Position == m_Length || !char.IsDigit(m_Text[m_Position])) return false; while (m_Position < m_Length) { var ch = m_Text[m_Position]; if (ch < '0' || ch > '9') break; fractionalPart = (ch - '0') / fractionalDivisor + fractionalPart; fractionalDivisor *= 10; ++m_Position; } } if (m_Position < m_Length && (m_Text[m_Position] == 'e' || m_Text[m_Position] == 'E')) { ++m_Position; var isNegative = false; if (m_Position < m_Length && m_Text[m_Position] == '-') { isNegative = true; ++m_Position; } else if (m_Position < m_Length && m_Text[m_Position] == '+') { ++m_Position; } var multiplier = 1; while (m_Position < m_Length && char.IsDigit(m_Text[m_Position])) { var digit = m_Text[m_Position] - '0'; exponent *= multiplier; exponent += digit; multiplier *= 10; ++m_Position; } if (isNegative) exponent *= -1; } if (!m_DryRun) { if (!haveFractionalPart && exponent == 0) { if (negative) result = -integralPart; else result = integralPart; } else { float value; if (negative) value = (float)-(integralPart + fractionalPart); else value = (float)(integralPart + fractionalPart); if (exponent != 0) value *= Mathf.Pow(10, exponent); result = value; } } return true; } public bool ParseBooleanValue(out JsonValue result) { SkipWhitespace(); if (SkipString("true")) { result = true; return true; } if (SkipString("false")) { result = false; return true; } result = default; return false; } public bool ParseNullValue(out JsonValue result) { result = default; return SkipString("null"); } public bool SkipToValue() { SkipWhitespace(); if (m_Position == m_Length || m_Text[m_Position] != ':') return false; ++m_Position; SkipWhitespace(); return true; } private bool SkipString(string text) { SkipWhitespace(); var length = text.Length; if (m_Position + length >= m_Length) return false; for (var i = 0; i < length; ++i) { if (m_Text[m_Position + i] != text[i]) return false; } m_Position += length; return true; } private void SkipWhitespace() { while (m_Position < m_Length && char.IsWhiteSpace(m_Text[m_Position])) ++m_Position; } public bool isAtEnd => m_Position >= m_Length; private readonly string m_Text; private readonly int m_Length; private int m_Position; private bool m_MatchAnyElementInArray; private bool m_DryRun; public enum JsonValueType { None, Bool, Real, Integer, String, Array, Object, Any, } public struct JsonString : IEquatable { public Substring text; public bool hasEscapes; public override string ToString() { if (!hasEscapes) return text.ToString(); var builder = new StringBuilder(); var length = text.length; for (var i = 0; i < length; ++i) { var ch = text[i]; if (ch == '\\') { ++i; if (i == length) break; ch = text[i]; } builder.Append(ch); } return builder.ToString(); } public bool Equals(JsonString other) { if (hasEscapes == other.hasEscapes) return Substring.Compare(text, other.text, StringComparison.InvariantCultureIgnoreCase) == 0; var thisLength = text.length; var otherLength = other.text.length; int thisIndex = 0, otherIndex = 0; for (; thisIndex < thisLength && otherIndex < otherLength; ++thisIndex, ++otherIndex) { var thisChar = text[thisIndex]; var otherChar = other.text[otherIndex]; if (thisChar == '\\') { ++thisIndex; if (thisIndex == thisLength) return false; thisChar = text[thisIndex]; } if (otherChar == '\\') { ++otherIndex; if (otherIndex == otherLength) return false; otherChar = other.text[otherIndex]; } if (char.ToUpperInvariant(thisChar) != char.ToUpperInvariant(otherChar)) return false; } return thisIndex == thisLength && otherIndex == otherLength; } public override bool Equals(object obj) { return obj is JsonString other && Equals(other); } public override int GetHashCode() { unchecked { return (text.GetHashCode() * 397) ^ hasEscapes.GetHashCode(); } } public static bool operator==(JsonString left, JsonString right) { return left.Equals(right); } public static bool operator!=(JsonString left, JsonString right) { return !left.Equals(right); } public static implicit operator JsonString(string str) { return new JsonString { text = str }; } } public struct JsonValue : IEquatable { public JsonValueType type; public bool boolValue; public double realValue; public long integerValue; public JsonString stringValue; public List arrayValue; // Allocates. public Dictionary objectValue; // Allocates. public object anyValue; public bool ToBoolean() { switch (type) { case JsonValueType.Bool: return boolValue; case JsonValueType.Integer: return integerValue != 0; case JsonValueType.Real: return NumberHelpers.Approximately(0, realValue); case JsonValueType.String: return Convert.ToBoolean(ToString()); } return default; } public long ToInteger() { switch (type) { case JsonValueType.Bool: return boolValue ? 1 : 0; case JsonValueType.Integer: return integerValue; case JsonValueType.Real: return (long)realValue; case JsonValueType.String: return Convert.ToInt64(ToString()); } return default; } public double ToDouble() { switch (type) { case JsonValueType.Bool: return boolValue ? 1 : 0; case JsonValueType.Integer: return integerValue; case JsonValueType.Real: return realValue; case JsonValueType.String: return Convert.ToSingle(ToString()); } return default; } public override string ToString() { switch (type) { case JsonValueType.None: return "null"; case JsonValueType.Bool: return boolValue.ToString(); case JsonValueType.Integer: return integerValue.ToString(CultureInfo.InvariantCulture); case JsonValueType.Real: return realValue.ToString(CultureInfo.InvariantCulture); case JsonValueType.String: return stringValue.ToString(); case JsonValueType.Array: if (arrayValue == null) return "[]"; return $"[{string.Join(",", arrayValue.Select(x => x.ToString()))}]"; case JsonValueType.Object: if (objectValue == null) return "{}"; var elements = objectValue.Select(pair => $"\"{pair.Key}\" : \"{pair.Value}\""); return $"{{{string.Join(",", elements)}}}"; case JsonValueType.Any: return anyValue.ToString(); } return base.ToString(); } public static implicit operator JsonValue(bool val) { return new JsonValue { type = JsonValueType.Bool, boolValue = val }; } public static implicit operator JsonValue(long val) { return new JsonValue { type = JsonValueType.Integer, integerValue = val }; } public static implicit operator JsonValue(double val) { return new JsonValue { type = JsonValueType.Real, realValue = val }; } public static implicit operator JsonValue(string str) { return new JsonValue { type = JsonValueType.String, stringValue = new JsonString { text = str } }; } public static implicit operator JsonValue(JsonString str) { return new JsonValue { type = JsonValueType.String, stringValue = str }; } public static implicit operator JsonValue(List array) { return new JsonValue { type = JsonValueType.Array, arrayValue = array }; } public static implicit operator JsonValue(Dictionary obj) { return new JsonValue { type = JsonValueType.Object, objectValue = obj }; } public static implicit operator JsonValue(Enum val) { return new JsonValue { type = JsonValueType.Any, anyValue = val }; } public bool Equals(JsonValue other) { // Default comparisons. if (type == other.type) { switch (type) { case JsonValueType.None: return true; case JsonValueType.Bool: return boolValue == other.boolValue; case JsonValueType.Integer: return integerValue == other.integerValue; case JsonValueType.Real: return NumberHelpers.Approximately(realValue, other.realValue); case JsonValueType.String: return stringValue == other.stringValue; case JsonValueType.Object: throw new NotImplementedException(); case JsonValueType.Array: throw new NotImplementedException(); case JsonValueType.Any: return anyValue.Equals(other.anyValue); } return false; } // anyValue-based comparisons. if (anyValue != null) return Equals(anyValue, other); if (other.anyValue != null) return Equals(other.anyValue, this); return false; } private static bool Equals(object obj, JsonValue value) { if (obj == null) return false; if (obj is Regex regex) return regex.IsMatch(value.ToString()); if (obj is string str) { switch (value.type) { case JsonValueType.String: return value.stringValue == str; case JsonValueType.Integer: return long.TryParse(str, out var si) && si == value.integerValue; case JsonValueType.Real: return double.TryParse(str, out var sf) && NumberHelpers.Approximately(sf, value.realValue); case JsonValueType.Bool: if (value.boolValue) return str == "True" || str == "true" || str == "1"; return str == "False" || str == "false" || str == "0"; } } if (obj is float f) { if (value.type == JsonValueType.Real) return NumberHelpers.Approximately(f, value.realValue); if (value.type == JsonValueType.String) return float.TryParse(value.ToString(), out var otherF) && Mathf.Approximately(f, otherF); } if (obj is double d) { if (value.type == JsonValueType.Real) return NumberHelpers.Approximately(d, value.realValue); if (value.type == JsonValueType.String) return double.TryParse(value.ToString(), out var otherD) && NumberHelpers.Approximately(d, otherD); } if (obj is int i) { if (value.type == JsonValueType.Integer) return i == value.integerValue; if (value.type == JsonValueType.String) return int.TryParse(value.ToString(), out var otherI) && i == otherI; } if (obj is long l) { if (value.type == JsonValueType.Integer) return l == value.integerValue; if (value.type == JsonValueType.String) return long.TryParse(value.ToString(), out var otherL) && l == otherL; } if (obj is bool b) { if (value.type == JsonValueType.Bool) return b == value.boolValue; if (value.type == JsonValueType.String) { if (b) return value.stringValue == "true" || value.stringValue == "True" || value.stringValue == "1"; return value.stringValue == "false" || value.stringValue == "False" || value.stringValue == "0"; } } // NOTE: The enum-based comparisons allocate both on the Convert.ToInt64() and Enum.GetName() path. I've found // no way to do either comparison in a way that does not allocate. if (obj is Enum) { if (value.type == JsonValueType.Integer) return Convert.ToInt64(obj) == value.integerValue; if (value.type == JsonValueType.String) return value.stringValue == Enum.GetName(obj.GetType(), obj); } return false; } public override bool Equals(object obj) { return obj is JsonValue other && Equals(other); } public override int GetHashCode() { unchecked { var hashCode = (int)type; hashCode = (hashCode * 397) ^ boolValue.GetHashCode(); hashCode = (hashCode * 397) ^ realValue.GetHashCode(); hashCode = (hashCode * 397) ^ integerValue.GetHashCode(); hashCode = (hashCode * 397) ^ stringValue.GetHashCode(); hashCode = (hashCode * 397) ^ (arrayValue != null ? arrayValue.GetHashCode() : 0); hashCode = (hashCode * 397) ^ (objectValue != null ? objectValue.GetHashCode() : 0); hashCode = (hashCode * 397) ^ (anyValue != null ? anyValue.GetHashCode() : 0); return hashCode; } } public static bool operator==(JsonValue left, JsonValue right) { return left.Equals(right); } public static bool operator!=(JsonValue left, JsonValue right) { return !left.Equals(right); } } } }