505 lines
17 KiB
C#
505 lines
17 KiB
C#
|
using System;
|
|||
|
using System.Collections.Generic;
|
|||
|
using System.Globalization;
|
|||
|
using System.Text;
|
|||
|
|
|||
|
namespace FullSerializer {
|
|||
|
// TODO: properly propagate warnings/etc for fsResult states
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// A simple recursive descent parser for JSON.
|
|||
|
/// </summary>
|
|||
|
public class fsJsonParser {
|
|||
|
private int _start;
|
|||
|
private string _input;
|
|||
|
|
|||
|
private fsResult MakeFailure(string message) {
|
|||
|
int start = Math.Max(0, _start - 20);
|
|||
|
int length = Math.Min(50, _input.Length - start);
|
|||
|
|
|||
|
string error = "Error while parsing: " + message + "; context = <" +
|
|||
|
_input.Substring(start, length) + ">";
|
|||
|
return fsResult.Fail(error);
|
|||
|
}
|
|||
|
|
|||
|
private bool TryMoveNext() {
|
|||
|
if (_start < _input.Length) {
|
|||
|
++_start;
|
|||
|
return true;
|
|||
|
}
|
|||
|
|
|||
|
return false;
|
|||
|
}
|
|||
|
|
|||
|
private bool HasValue() {
|
|||
|
return HasValue(0);
|
|||
|
}
|
|||
|
|
|||
|
private bool HasValue(int offset) {
|
|||
|
return (_start + offset) >= 0 && (_start + offset) < _input.Length;
|
|||
|
}
|
|||
|
|
|||
|
private char Character() {
|
|||
|
return Character(0);
|
|||
|
}
|
|||
|
|
|||
|
private char Character(int offset) {
|
|||
|
return _input[_start + offset];
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Skips input such that Character() will return a non-whitespace character
|
|||
|
/// </summary>
|
|||
|
private void SkipSpace() {
|
|||
|
while (HasValue()) {
|
|||
|
char c = Character();
|
|||
|
|
|||
|
// whitespace; fine to skip
|
|||
|
if (char.IsWhiteSpace(c)) {
|
|||
|
TryMoveNext();
|
|||
|
continue;
|
|||
|
}
|
|||
|
|
|||
|
// comment?
|
|||
|
if (HasValue(1) && Character(0) == '/') {
|
|||
|
if (Character(1) == '/') {
|
|||
|
// skip the rest of the line
|
|||
|
while (HasValue() && Environment.NewLine.Contains("" + Character()) == false) {
|
|||
|
TryMoveNext();
|
|||
|
}
|
|||
|
continue;
|
|||
|
} else if (Character(1) == '*') {
|
|||
|
// skip to comment close
|
|||
|
TryMoveNext();
|
|||
|
TryMoveNext();
|
|||
|
while (HasValue(1)) {
|
|||
|
if (Character(0) == '*' && Character(1) == '/') {
|
|||
|
TryMoveNext();
|
|||
|
TryMoveNext();
|
|||
|
TryMoveNext();
|
|||
|
break;
|
|||
|
} else {
|
|||
|
TryMoveNext();
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
// let other checks to check fail
|
|||
|
continue;
|
|||
|
}
|
|||
|
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
#region Escaping
|
|||
|
private bool IsHex(char c) {
|
|||
|
return ((c >= '0' && c <= '9') ||
|
|||
|
(c >= 'a' && c <= 'f') ||
|
|||
|
(c >= 'A' && c <= 'F'));
|
|||
|
}
|
|||
|
|
|||
|
private uint ParseSingleChar(char c1, uint multipliyer) {
|
|||
|
uint p1 = 0;
|
|||
|
if (c1 >= '0' && c1 <= '9')
|
|||
|
p1 = (uint)(c1 - '0') * multipliyer;
|
|||
|
else if (c1 >= 'A' && c1 <= 'F')
|
|||
|
p1 = (uint)((c1 - 'A') + 10) * multipliyer;
|
|||
|
else if (c1 >= 'a' && c1 <= 'f')
|
|||
|
p1 = (uint)((c1 - 'a') + 10) * multipliyer;
|
|||
|
return p1;
|
|||
|
}
|
|||
|
|
|||
|
private uint ParseUnicode(char c1, char c2, char c3, char c4) {
|
|||
|
uint p1 = ParseSingleChar(c1, 0x1000);
|
|||
|
uint p2 = ParseSingleChar(c2, 0x100);
|
|||
|
uint p3 = ParseSingleChar(c3, 0x10);
|
|||
|
uint p4 = ParseSingleChar(c4, 0x1);
|
|||
|
|
|||
|
return p1 + p2 + p3 + p4;
|
|||
|
}
|
|||
|
|
|||
|
private fsResult TryUnescapeChar(out char escaped) {
|
|||
|
// skip leading backslash '\'
|
|||
|
TryMoveNext();
|
|||
|
if (HasValue() == false) {
|
|||
|
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 codePoint = ParseUnicode(Character(0), Character(1), Character(2), Character(3));
|
|||
|
|
|||
|
TryMoveNext();
|
|||
|
TryMoveNext();
|
|||
|
TryMoveNext();
|
|||
|
TryMoveNext();
|
|||
|
|
|||
|
escaped = (char)codePoint;
|
|||
|
return fsResult.Success;
|
|||
|
}
|
|||
|
|
|||
|
// invalid escape sequence
|
|||
|
escaped = (char)0;
|
|||
|
return MakeFailure(
|
|||
|
string.Format("invalid escape sequence '\\u{0}{1}{2}{3}'\n",
|
|||
|
Character(0),
|
|||
|
Character(1),
|
|||
|
Character(2),
|
|||
|
Character(3)));
|
|||
|
default:
|
|||
|
escaped = (char)0;
|
|||
|
return MakeFailure(string.Format("Invalid escape sequence \\{0}", Character()));
|
|||
|
}
|
|||
|
}
|
|||
|
#endregion
|
|||
|
|
|||
|
private fsResult TryParseExact(string content) {
|
|||
|
for (int i = 0; i < content.Length; ++i) {
|
|||
|
if (Character() != content[i]) {
|
|||
|
return MakeFailure("Expected " + content[i]);
|
|||
|
}
|
|||
|
|
|||
|
if (TryMoveNext() == false) {
|
|||
|
return MakeFailure("Unexpected end of content when parsing " + content);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
return fsResult.Success;
|
|||
|
}
|
|||
|
|
|||
|
private fsResult TryParseTrue(out fsData data) {
|
|||
|
var fail = TryParseExact("true");
|
|||
|
|
|||
|
if (fail.Succeeded) {
|
|||
|
data = new fsData(true);
|
|||
|
return fsResult.Success;
|
|||
|
}
|
|||
|
|
|||
|
data = null;
|
|||
|
return fail;
|
|||
|
}
|
|||
|
|
|||
|
private fsResult TryParseFalse(out fsData data) {
|
|||
|
var fail = TryParseExact("false");
|
|||
|
|
|||
|
if (fail.Succeeded) {
|
|||
|
data = new fsData(false);
|
|||
|
return fsResult.Success;
|
|||
|
}
|
|||
|
|
|||
|
data = null;
|
|||
|
return fail;
|
|||
|
}
|
|||
|
|
|||
|
private fsResult TryParseNull(out fsData data) {
|
|||
|
var fail = TryParseExact("null");
|
|||
|
|
|||
|
if (fail.Succeeded) {
|
|||
|
data = new fsData();
|
|||
|
return fsResult.Success;
|
|||
|
}
|
|||
|
|
|||
|
data = null;
|
|||
|
return fail;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
private bool IsSeparator(char c) {
|
|||
|
return char.IsWhiteSpace(c) || c == ',' || c == '}' || c == ']';
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Parses numbers that follow the regular expression [-+](\d+|\d*\.\d*)
|
|||
|
/// </summary>
|
|||
|
private fsResult TryParseNumber(out fsData data) {
|
|||
|
int start = _start;
|
|||
|
|
|||
|
// read until we get to a separator
|
|||
|
while (
|
|||
|
TryMoveNext() &&
|
|||
|
(HasValue() && IsSeparator(Character()) == false)) {
|
|||
|
}
|
|||
|
|
|||
|
// try to parse the value
|
|||
|
string numberString = _input.Substring(start, _start - start);
|
|||
|
|
|||
|
// double -- includes a .
|
|||
|
if (numberString.Contains(".") || numberString.Contains("e") || numberString.Contains("E") ||
|
|||
|
numberString == "Infinity" || numberString == "-Infinity" || numberString == "NaN") {
|
|||
|
double doubleValue;
|
|||
|
if (double.TryParse(numberString, NumberStyles.Any, CultureInfo.InvariantCulture, out doubleValue) == false) {
|
|||
|
data = null;
|
|||
|
return MakeFailure("Bad double format with " + numberString);
|
|||
|
}
|
|||
|
|
|||
|
data = new fsData(doubleValue);
|
|||
|
return fsResult.Success;
|
|||
|
}
|
|||
|
else {
|
|||
|
Int64 intValue;
|
|||
|
if (Int64.TryParse(numberString, NumberStyles.Any, CultureInfo.InvariantCulture, out intValue) == false) {
|
|||
|
data = null;
|
|||
|
return MakeFailure("Bad Int64 format with " + numberString);
|
|||
|
}
|
|||
|
|
|||
|
data = new fsData(intValue);
|
|||
|
return fsResult.Success;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
private readonly StringBuilder _cachedStringBuilder = new StringBuilder(256);
|
|||
|
/// <summary>
|
|||
|
/// Parses a string
|
|||
|
/// </summary>
|
|||
|
private fsResult TryParseString(out string str) {
|
|||
|
_cachedStringBuilder.Length = 0;
|
|||
|
|
|||
|
// skip the first "
|
|||
|
if (Character() != '"' || TryMoveNext() == false) {
|
|||
|
str = string.Empty;
|
|||
|
return MakeFailure("Expected initial \" when parsing a string");
|
|||
|
}
|
|||
|
|
|||
|
// read until the next "
|
|||
|
while (HasValue() && Character() != '\"') {
|
|||
|
char c = Character();
|
|||
|
|
|||
|
// escape if necessary
|
|||
|
if (c == '\\') {
|
|||
|
char unescaped;
|
|||
|
var fail = TryUnescapeChar(out unescaped);
|
|||
|
if (fail.Failed) {
|
|||
|
str = string.Empty;
|
|||
|
return fail;
|
|||
|
}
|
|||
|
|
|||
|
_cachedStringBuilder.Append(unescaped);
|
|||
|
}
|
|||
|
|
|||
|
// no escaping necessary
|
|||
|
else {
|
|||
|
_cachedStringBuilder.Append(c);
|
|||
|
|
|||
|
// get the next character
|
|||
|
if (TryMoveNext() == false) {
|
|||
|
str = string.Empty;
|
|||
|
return MakeFailure("Unexpected end of input when reading a string");
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// skip the first "
|
|||
|
if (HasValue() == false || Character() != '"' || TryMoveNext() == false) {
|
|||
|
str = string.Empty;
|
|||
|
return MakeFailure("No closing \" when parsing a string");
|
|||
|
}
|
|||
|
|
|||
|
str = _cachedStringBuilder.ToString();
|
|||
|
return fsResult.Success;
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Parses an array
|
|||
|
/// </summary>
|
|||
|
private fsResult TryParseArray(out fsData arr) {
|
|||
|
if (Character() != '[') {
|
|||
|
arr = null;
|
|||
|
return MakeFailure("Expected initial [ when parsing an array");
|
|||
|
}
|
|||
|
|
|||
|
// skip '['
|
|||
|
if (TryMoveNext() == false) {
|
|||
|
arr = null;
|
|||
|
return MakeFailure("Unexpected end of input when parsing an array");
|
|||
|
}
|
|||
|
SkipSpace();
|
|||
|
|
|||
|
var result = new List<fsData>();
|
|||
|
|
|||
|
while (HasValue() && Character() != ']') {
|
|||
|
// parse the element
|
|||
|
fsData element;
|
|||
|
var fail = RunParse(out element);
|
|||
|
if (fail.Failed) {
|
|||
|
arr = null;
|
|||
|
return fail;
|
|||
|
}
|
|||
|
|
|||
|
result.Add(element);
|
|||
|
|
|||
|
// parse the comma
|
|||
|
SkipSpace();
|
|||
|
if (HasValue() && Character() == ',') {
|
|||
|
if (TryMoveNext() == false) break;
|
|||
|
SkipSpace();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// skip the final ]
|
|||
|
if (HasValue() == false || Character() != ']' || TryMoveNext() == false) {
|
|||
|
arr = null;
|
|||
|
return MakeFailure("No closing ] for array");
|
|||
|
}
|
|||
|
|
|||
|
arr = new fsData(result);
|
|||
|
return fsResult.Success;
|
|||
|
}
|
|||
|
|
|||
|
private fsResult TryParseObject(out fsData obj) {
|
|||
|
if (Character() != '{') {
|
|||
|
obj = null;
|
|||
|
return MakeFailure("Expected initial { when parsing an object");
|
|||
|
}
|
|||
|
|
|||
|
// skip '{'
|
|||
|
if (TryMoveNext() == false) {
|
|||
|
obj = null;
|
|||
|
return MakeFailure("Unexpected end of input when parsing an object");
|
|||
|
}
|
|||
|
SkipSpace();
|
|||
|
|
|||
|
var result = new Dictionary<string, fsData>(
|
|||
|
fsGlobalConfig.IsCaseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase);
|
|||
|
|
|||
|
while (HasValue() && Character() != '}') {
|
|||
|
fsResult failure;
|
|||
|
|
|||
|
// parse the key
|
|||
|
SkipSpace();
|
|||
|
string key;
|
|||
|
failure = TryParseString(out key);
|
|||
|
if (failure.Failed) {
|
|||
|
obj = null;
|
|||
|
return failure;
|
|||
|
}
|
|||
|
SkipSpace();
|
|||
|
|
|||
|
// parse the ':' after the key
|
|||
|
if (HasValue() == false || Character() != ':' || TryMoveNext() == false) {
|
|||
|
obj = null;
|
|||
|
return MakeFailure("Expected : after key \"" + key + "\"");
|
|||
|
}
|
|||
|
SkipSpace();
|
|||
|
|
|||
|
// parse the value
|
|||
|
fsData value;
|
|||
|
failure = RunParse(out value);
|
|||
|
if (failure.Failed) {
|
|||
|
obj = null;
|
|||
|
return failure;
|
|||
|
}
|
|||
|
|
|||
|
result.Add(key, value);
|
|||
|
|
|||
|
// parse the comma
|
|||
|
SkipSpace();
|
|||
|
if (HasValue() && Character() == ',') {
|
|||
|
if (TryMoveNext() == false) break;
|
|||
|
SkipSpace();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// skip the final }
|
|||
|
if (HasValue() == false || Character() != '}' || TryMoveNext() == false) {
|
|||
|
obj = null;
|
|||
|
return MakeFailure("No closing } for object");
|
|||
|
}
|
|||
|
|
|||
|
obj = new fsData(result);
|
|||
|
return fsResult.Success;
|
|||
|
}
|
|||
|
|
|||
|
private fsResult RunParse(out fsData data) {
|
|||
|
SkipSpace();
|
|||
|
|
|||
|
if (HasValue() == false) {
|
|||
|
data = default(fsData);
|
|||
|
return MakeFailure("Unexpected end of input");
|
|||
|
}
|
|||
|
|
|||
|
switch (Character()) {
|
|||
|
case 'I': // Infinity
|
|||
|
case 'N': // NaN
|
|||
|
case '.':
|
|||
|
case '+':
|
|||
|
case '-':
|
|||
|
case '0':
|
|||
|
case '1':
|
|||
|
case '2':
|
|||
|
case '3':
|
|||
|
case '4':
|
|||
|
case '5':
|
|||
|
case '6':
|
|||
|
case '7':
|
|||
|
case '8':
|
|||
|
case '9': return TryParseNumber(out data);
|
|||
|
case '"': {
|
|||
|
string str;
|
|||
|
fsResult fail = TryParseString(out str);
|
|||
|
if (fail.Failed) {
|
|||
|
data = null;
|
|||
|
return fail;
|
|||
|
}
|
|||
|
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() + "\"");
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Parses the specified input. Returns a failure state if parsing failed.
|
|||
|
/// </summary>
|
|||
|
/// <param name="input">The input to parse.</param>
|
|||
|
/// <param name="data">The parsed data. This is undefined if parsing fails.</param>
|
|||
|
/// <returns>The parsed input.</returns>
|
|||
|
public static fsResult Parse(string input, out fsData data) {
|
|||
|
if (string.IsNullOrEmpty(input)) {
|
|||
|
data = default(fsData);
|
|||
|
return fsResult.Fail("No input");
|
|||
|
}
|
|||
|
|
|||
|
var context = new fsJsonParser(input);
|
|||
|
return context.RunParse(out data);
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Helper method for Parse that does not allow the error information
|
|||
|
/// to be recovered.
|
|||
|
/// </summary>
|
|||
|
public static fsData Parse(string input) {
|
|||
|
fsData data;
|
|||
|
Parse(input, out data).AssertSuccess();
|
|||
|
return data;
|
|||
|
}
|
|||
|
|
|||
|
private fsJsonParser(string input) {
|
|||
|
_input = input;
|
|||
|
_start = 0;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|