524 lines
11 KiB
C#
524 lines
11 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Text;
|
|
|
|
namespace Gaia.FullSerializer
|
|
{
|
|
public class fsJsonParser
|
|
{
|
|
private int _start;
|
|
|
|
private string _input;
|
|
|
|
private readonly StringBuilder _cachedStringBuilder = new StringBuilder(256);
|
|
|
|
private fsResult MakeFailure(string message)
|
|
{
|
|
int num = Math.Max(0, _start - 20);
|
|
int length = Math.Min(50, _input.Length - num);
|
|
return fsResult.Fail("Error while parsing: " + message + "; context = <" + _input.Substring(num, length) + ">");
|
|
}
|
|
|
|
private bool TryMoveNext()
|
|
{
|
|
if (_start < _input.Length)
|
|
{
|
|
_start++;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private bool HasValue()
|
|
{
|
|
return HasValue(0);
|
|
}
|
|
|
|
private bool HasValue(int offset)
|
|
{
|
|
if (_start + offset >= 0)
|
|
{
|
|
return _start + offset < _input.Length;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private char Character()
|
|
{
|
|
return Character(0);
|
|
}
|
|
|
|
private char Character(int offset)
|
|
{
|
|
return _input[_start + offset];
|
|
}
|
|
|
|
private void SkipSpace()
|
|
{
|
|
while (HasValue())
|
|
{
|
|
if (char.IsWhiteSpace(Character()))
|
|
{
|
|
TryMoveNext();
|
|
continue;
|
|
}
|
|
if (!HasValue(1) || Character(0) != '/')
|
|
{
|
|
break;
|
|
}
|
|
if (Character(1) == '/')
|
|
{
|
|
while (HasValue() && !Environment.NewLine.Contains(Character().ToString() ?? ""))
|
|
{
|
|
TryMoveNext();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (Character(1) != '*')
|
|
{
|
|
continue;
|
|
}
|
|
TryMoveNext();
|
|
TryMoveNext();
|
|
while (HasValue(1))
|
|
{
|
|
if (Character(0) == '*' && Character(1) == '/')
|
|
{
|
|
TryMoveNext();
|
|
TryMoveNext();
|
|
TryMoveNext();
|
|
break;
|
|
}
|
|
TryMoveNext();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool IsHex(char c)
|
|
{
|
|
if ((c < '0' || c > '9') && (c < 'a' || c > 'f'))
|
|
{
|
|
if (c >= 'A')
|
|
{
|
|
return c <= 'F';
|
|
}
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private uint ParseSingleChar(char c1, uint multipliyer)
|
|
{
|
|
uint result = 0u;
|
|
if (c1 >= '0' && c1 <= '9')
|
|
{
|
|
result = (uint)(c1 - 48) * multipliyer;
|
|
}
|
|
else if (c1 >= 'A' && c1 <= 'F')
|
|
{
|
|
result = (uint)(c1 - 65 + 10) * multipliyer;
|
|
}
|
|
else if (c1 >= 'a' && c1 <= 'f')
|
|
{
|
|
result = (uint)(c1 - 97 + 10) * multipliyer;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private uint ParseUnicode(char c1, char c2, char c3, char c4)
|
|
{
|
|
uint num = ParseSingleChar(c1, 4096u);
|
|
uint num2 = ParseSingleChar(c2, 256u);
|
|
uint num3 = ParseSingleChar(c3, 16u);
|
|
uint num4 = ParseSingleChar(c4, 1u);
|
|
return num + num2 + num3 + num4;
|
|
}
|
|
|
|
private fsResult TryUnescapeChar(out char escaped)
|
|
{
|
|
TryMoveNext();
|
|
if (!HasValue())
|
|
{
|
|
escaped = ' ';
|
|
return MakeFailure("Unexpected end of input after \\");
|
|
}
|
|
switch (Character())
|
|
{
|
|
case '\\':
|
|
TryMoveNext();
|
|
escaped = '\\';
|
|
return fsResult.Success;
|
|
case '/':
|
|
TryMoveNext();
|
|
escaped = '/';
|
|
return fsResult.Success;
|
|
case '"':
|
|
TryMoveNext();
|
|
escaped = '"';
|
|
return fsResult.Success;
|
|
case 'a':
|
|
TryMoveNext();
|
|
escaped = '\a';
|
|
return fsResult.Success;
|
|
case 'b':
|
|
TryMoveNext();
|
|
escaped = '\b';
|
|
return fsResult.Success;
|
|
case 'f':
|
|
TryMoveNext();
|
|
escaped = '\f';
|
|
return fsResult.Success;
|
|
case 'n':
|
|
TryMoveNext();
|
|
escaped = '\n';
|
|
return fsResult.Success;
|
|
case 'r':
|
|
TryMoveNext();
|
|
escaped = '\r';
|
|
return fsResult.Success;
|
|
case 't':
|
|
TryMoveNext();
|
|
escaped = '\t';
|
|
return fsResult.Success;
|
|
case '0':
|
|
TryMoveNext();
|
|
escaped = '\0';
|
|
return fsResult.Success;
|
|
case 'u':
|
|
TryMoveNext();
|
|
if (IsHex(Character(0)) && IsHex(Character(1)) && IsHex(Character(2)) && IsHex(Character(3)))
|
|
{
|
|
uint num = ParseUnicode(Character(0), Character(1), Character(2), Character(3));
|
|
TryMoveNext();
|
|
TryMoveNext();
|
|
TryMoveNext();
|
|
TryMoveNext();
|
|
escaped = (char)num;
|
|
return fsResult.Success;
|
|
}
|
|
escaped = '\0';
|
|
return MakeFailure($"invalid escape sequence '\\u{Character(0)}{Character(1)}{Character(2)}{Character(3)}'\n");
|
|
default:
|
|
escaped = '\0';
|
|
return MakeFailure($"Invalid escape sequence \\{Character()}");
|
|
}
|
|
}
|
|
|
|
private fsResult TryParseExact(string content)
|
|
{
|
|
for (int i = 0; i < content.Length; i++)
|
|
{
|
|
if (Character() != content[i])
|
|
{
|
|
return MakeFailure("Expected " + content[i]);
|
|
}
|
|
if (!TryMoveNext())
|
|
{
|
|
return MakeFailure("Unexpected end of content when parsing " + content);
|
|
}
|
|
}
|
|
return fsResult.Success;
|
|
}
|
|
|
|
private fsResult TryParseTrue(out fsData data)
|
|
{
|
|
fsResult result = TryParseExact("true");
|
|
if (result.Succeeded)
|
|
{
|
|
data = new fsData(boolean: true);
|
|
return fsResult.Success;
|
|
}
|
|
data = null;
|
|
return result;
|
|
}
|
|
|
|
private fsResult TryParseFalse(out fsData data)
|
|
{
|
|
fsResult result = TryParseExact("false");
|
|
if (result.Succeeded)
|
|
{
|
|
data = new fsData(boolean: false);
|
|
return fsResult.Success;
|
|
}
|
|
data = null;
|
|
return result;
|
|
}
|
|
|
|
private fsResult TryParseNull(out fsData data)
|
|
{
|
|
fsResult result = TryParseExact("null");
|
|
if (result.Succeeded)
|
|
{
|
|
data = new fsData();
|
|
return fsResult.Success;
|
|
}
|
|
data = null;
|
|
return result;
|
|
}
|
|
|
|
private bool IsSeparator(char c)
|
|
{
|
|
if (!char.IsWhiteSpace(c) && c != ',' && c != '}')
|
|
{
|
|
return c == ']';
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private fsResult TryParseNumber(out fsData data)
|
|
{
|
|
int start = _start;
|
|
while (TryMoveNext() && HasValue() && !IsSeparator(Character()))
|
|
{
|
|
}
|
|
string text = _input.Substring(start, _start - start);
|
|
if (!text.Contains("."))
|
|
{
|
|
switch (text)
|
|
{
|
|
case "Infinity":
|
|
case "-Infinity":
|
|
case "NaN":
|
|
break;
|
|
default:
|
|
{
|
|
if (!ulong.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
|
|
{
|
|
data = null;
|
|
return MakeFailure("Bad UInt64 format with " + text);
|
|
}
|
|
data = new fsData(result);
|
|
return fsResult.Success;
|
|
}
|
|
}
|
|
}
|
|
if (!double.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out var result2))
|
|
{
|
|
data = null;
|
|
return MakeFailure("Bad double format with " + text);
|
|
}
|
|
data = new fsData(result2);
|
|
return fsResult.Success;
|
|
}
|
|
|
|
private fsResult TryParseString(out string str)
|
|
{
|
|
_cachedStringBuilder.Length = 0;
|
|
if (Character() != '"' || !TryMoveNext())
|
|
{
|
|
str = string.Empty;
|
|
return MakeFailure("Expected initial \" when parsing a string");
|
|
}
|
|
while (HasValue() && Character() != '"')
|
|
{
|
|
char c = Character();
|
|
if (c == '\\')
|
|
{
|
|
char escaped;
|
|
fsResult result = TryUnescapeChar(out escaped);
|
|
if (result.Failed)
|
|
{
|
|
str = string.Empty;
|
|
return result;
|
|
}
|
|
_cachedStringBuilder.Append(escaped);
|
|
}
|
|
else
|
|
{
|
|
_cachedStringBuilder.Append(c);
|
|
if (!TryMoveNext())
|
|
{
|
|
str = string.Empty;
|
|
return MakeFailure("Unexpected end of input when reading a string");
|
|
}
|
|
}
|
|
}
|
|
if (!HasValue() || Character() != '"' || !TryMoveNext())
|
|
{
|
|
str = string.Empty;
|
|
return MakeFailure("No closing \" when parsing a string");
|
|
}
|
|
str = _cachedStringBuilder.ToString();
|
|
return fsResult.Success;
|
|
}
|
|
|
|
private fsResult TryParseArray(out fsData arr)
|
|
{
|
|
if (Character() != '[')
|
|
{
|
|
arr = null;
|
|
return MakeFailure("Expected initial [ when parsing an array");
|
|
}
|
|
if (!TryMoveNext())
|
|
{
|
|
arr = null;
|
|
return MakeFailure("Unexpected end of input when parsing an array");
|
|
}
|
|
SkipSpace();
|
|
List<fsData> list = new List<fsData>();
|
|
while (HasValue() && Character() != ']')
|
|
{
|
|
fsData data;
|
|
fsResult result = RunParse(out data);
|
|
if (result.Failed)
|
|
{
|
|
arr = null;
|
|
return result;
|
|
}
|
|
list.Add(data);
|
|
SkipSpace();
|
|
if (HasValue() && Character() == ',')
|
|
{
|
|
if (!TryMoveNext())
|
|
{
|
|
break;
|
|
}
|
|
SkipSpace();
|
|
}
|
|
}
|
|
if (!HasValue() || Character() != ']' || !TryMoveNext())
|
|
{
|
|
arr = null;
|
|
return MakeFailure("No closing ] for array");
|
|
}
|
|
arr = new fsData(list);
|
|
return fsResult.Success;
|
|
}
|
|
|
|
private fsResult TryParseObject(out fsData obj)
|
|
{
|
|
if (Character() != '{')
|
|
{
|
|
obj = null;
|
|
return MakeFailure("Expected initial { when parsing an object");
|
|
}
|
|
if (!TryMoveNext())
|
|
{
|
|
obj = null;
|
|
return MakeFailure("Unexpected end of input when parsing an object");
|
|
}
|
|
SkipSpace();
|
|
Dictionary<string, fsData> dictionary = new Dictionary<string, fsData>(fsConfig.IsCaseSensitive ? StringComparer.CurrentCulture : StringComparer.CurrentCultureIgnoreCase);
|
|
while (HasValue() && Character() != '}')
|
|
{
|
|
SkipSpace();
|
|
fsResult result = TryParseString(out var str);
|
|
if (result.Failed)
|
|
{
|
|
obj = null;
|
|
return result;
|
|
}
|
|
SkipSpace();
|
|
if (!HasValue() || Character() != ':' || !TryMoveNext())
|
|
{
|
|
obj = null;
|
|
return MakeFailure("Expected : after key \"" + str + "\"");
|
|
}
|
|
SkipSpace();
|
|
result = RunParse(out var data);
|
|
if (result.Failed)
|
|
{
|
|
obj = null;
|
|
return result;
|
|
}
|
|
dictionary.Add(str, data);
|
|
SkipSpace();
|
|
if (HasValue() && Character() == ',')
|
|
{
|
|
if (!TryMoveNext())
|
|
{
|
|
break;
|
|
}
|
|
SkipSpace();
|
|
}
|
|
}
|
|
if (!HasValue() || Character() != '}' || !TryMoveNext())
|
|
{
|
|
obj = null;
|
|
return MakeFailure("No closing } for object");
|
|
}
|
|
obj = new fsData(dictionary);
|
|
return fsResult.Success;
|
|
}
|
|
|
|
private fsResult RunParse(out fsData data)
|
|
{
|
|
SkipSpace();
|
|
if (!HasValue())
|
|
{
|
|
data = null;
|
|
return MakeFailure("Unexpected end of input");
|
|
}
|
|
switch (Character())
|
|
{
|
|
case '+':
|
|
case '-':
|
|
case '.':
|
|
case '0':
|
|
case '1':
|
|
case '2':
|
|
case '3':
|
|
case '4':
|
|
case '5':
|
|
case '6':
|
|
case '7':
|
|
case '8':
|
|
case '9':
|
|
case 'I':
|
|
case 'N':
|
|
return TryParseNumber(out data);
|
|
case '"':
|
|
{
|
|
string str;
|
|
fsResult result = TryParseString(out str);
|
|
if (result.Failed)
|
|
{
|
|
data = null;
|
|
return result;
|
|
}
|
|
data = new fsData(str);
|
|
return fsResult.Success;
|
|
}
|
|
case '[':
|
|
return TryParseArray(out data);
|
|
case '{':
|
|
return TryParseObject(out data);
|
|
case 't':
|
|
return TryParseTrue(out data);
|
|
case 'f':
|
|
return TryParseFalse(out data);
|
|
case 'n':
|
|
return TryParseNull(out data);
|
|
default:
|
|
data = null;
|
|
return MakeFailure("unable to parse; invalid token \"" + Character() + "\"");
|
|
}
|
|
}
|
|
|
|
public static fsResult Parse(string input, out fsData data)
|
|
{
|
|
if (string.IsNullOrEmpty(input))
|
|
{
|
|
data = null;
|
|
return fsResult.Fail("No input");
|
|
}
|
|
return new fsJsonParser(input).RunParse(out data);
|
|
}
|
|
|
|
public static fsData Parse(string input)
|
|
{
|
|
Parse(input, out var data).AssertSuccess();
|
|
return data;
|
|
}
|
|
|
|
private fsJsonParser(string input)
|
|
{
|
|
_input = input;
|
|
_start = 0;
|
|
}
|
|
}
|
|
}
|