UnityGame/Library/PackageCache/com.unity.inputsystem/InputSystem/Utilities/JsonParser.cs

921 lines
31 KiB
C#
Raw Normal View History

2024-10-27 10:53:47 +03:00
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
namespace UnityEngine.InputSystem.Utilities
{
/// <summary>
/// A JSON parser that instead of turning a string in JSON format into a
/// C# object graph, allows navigating the source text directly.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
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();
}
/// <summary>
/// Navigate to the given property.
/// </summary>
/// <param name="path"></param>
/// <remarks>
/// This navigates from the current property.
/// </remarks>
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;
}
/// <summary>
/// Return true if the current property has a value matching <paramref name="expectedValue"/>.
/// </summary>
/// <param name="expectedValue"></param>
/// <returns></returns>
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<JsonValue> values = null;
if (!m_DryRun)
values = new List<JsonValue>();
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<JsonString>
{
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<JsonValue>
{
public JsonValueType type;
public bool boolValue;
public double realValue;
public long integerValue;
public JsonString stringValue;
public List<JsonValue> arrayValue; // Allocates.
public Dictionary<string, JsonValue> 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<JsonValue> array)
{
return new JsonValue
{
type = JsonValueType.Array,
arrayValue = array
};
}
public static implicit operator JsonValue(Dictionary<string, JsonValue> 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);
}
}
}
}