333 lines
11 KiB
C#
333 lines
11 KiB
C#
#nullable enable
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Text;
|
|
|
|
namespace Swan.Formatters {
|
|
/// <summary>
|
|
/// A very simple, light-weight JSON library written by Mario
|
|
/// to teach Geo how things are done
|
|
///
|
|
/// This is an useful helper for small tasks but it doesn't represent a full-featured
|
|
/// serializer such as the beloved Json.NET.
|
|
/// </summary>
|
|
public partial class Json {
|
|
/// <summary>
|
|
/// A simple JSON Deserializer.
|
|
/// </summary>
|
|
private class Deserializer {
|
|
#region State Variables
|
|
|
|
private readonly Object? _result;
|
|
private readonly String _json;
|
|
|
|
private Dictionary<String, Object?>? _resultObject;
|
|
private List<Object?>? _resultArray;
|
|
private ReadState _state = ReadState.WaitingForRootOpen;
|
|
private String? _currentFieldName;
|
|
|
|
private Int32 _index;
|
|
|
|
#endregion
|
|
|
|
private Deserializer(String? json, Int32 startIndex) {
|
|
if(json == null) {
|
|
this._json = "";
|
|
return;
|
|
}
|
|
this._json = json;
|
|
|
|
for(this._index = startIndex; this._index < this._json.Length; this._index++) {
|
|
switch(this._state) {
|
|
case ReadState.WaitingForRootOpen:
|
|
this.WaitForRootOpen();
|
|
continue;
|
|
case ReadState.WaitingForField when Char.IsWhiteSpace(this._json, this._index):
|
|
continue;
|
|
case ReadState.WaitingForField when this._resultObject != null && this._json[this._index] == CloseObjectChar || this._resultArray != null && this._json[this._index] == CloseArrayChar:
|
|
// Handle empty arrays and empty objects
|
|
this._result = this._resultObject ?? this._resultArray as Object;
|
|
return;
|
|
case ReadState.WaitingForField when this._json[this._index] != StringQuotedChar:
|
|
throw this.CreateParserException($"'{StringQuotedChar}'");
|
|
case ReadState.WaitingForField: {
|
|
Int32 charCount = this.GetFieldNameCount();
|
|
|
|
this._currentFieldName = Unescape(this._json.SliceLength(this._index + 1, charCount));
|
|
this._index += charCount + 1;
|
|
this._state = ReadState.WaitingForColon;
|
|
continue;
|
|
}
|
|
|
|
case ReadState.WaitingForColon when Char.IsWhiteSpace(this._json, this._index):
|
|
continue;
|
|
case ReadState.WaitingForColon when this._json[this._index] != ValueSeparatorChar:
|
|
throw this.CreateParserException($"'{ValueSeparatorChar}'");
|
|
case ReadState.WaitingForColon:
|
|
this._state = ReadState.WaitingForValue;
|
|
continue;
|
|
case ReadState.WaitingForValue when Char.IsWhiteSpace(this._json, this._index):
|
|
continue;
|
|
case ReadState.WaitingForValue when this._resultObject != null && this._json[this._index] == CloseObjectChar || this._resultArray != null && this._json[this._index] == CloseArrayChar:
|
|
// Handle empty arrays and empty objects
|
|
this._result = this._resultObject ?? this._resultArray as Object;
|
|
return;
|
|
case ReadState.WaitingForValue:
|
|
this.ExtractValue();
|
|
continue;
|
|
}
|
|
|
|
if(this._state != ReadState.WaitingForNextOrRootClose || Char.IsWhiteSpace(this._json, this._index)) {
|
|
continue;
|
|
}
|
|
|
|
if(this._json[this._index] == FieldSeparatorChar) {
|
|
if(this._resultObject != null) {
|
|
this._state = ReadState.WaitingForField;
|
|
this._currentFieldName = null;
|
|
continue;
|
|
}
|
|
|
|
this._state = ReadState.WaitingForValue;
|
|
continue;
|
|
}
|
|
|
|
if((this._resultObject == null || this._json[this._index] != CloseObjectChar) && (this._resultArray == null || this._json[this._index] != CloseArrayChar)) {
|
|
throw this.CreateParserException($"'{FieldSeparatorChar}' '{CloseObjectChar}' or '{CloseArrayChar}'");
|
|
}
|
|
|
|
this._result = this._resultObject ?? this._resultArray as Object;
|
|
return;
|
|
}
|
|
}
|
|
|
|
internal static Object? DeserializeInternal(String? json) => new Deserializer(json, 0)._result;
|
|
|
|
private void WaitForRootOpen() {
|
|
if(Char.IsWhiteSpace(this._json, this._index)) {
|
|
return;
|
|
}
|
|
|
|
switch(this._json[this._index]) {
|
|
case OpenObjectChar:
|
|
this._resultObject = new Dictionary<String, Object?>();
|
|
this._state = ReadState.WaitingForField;
|
|
return;
|
|
case OpenArrayChar:
|
|
this._resultArray = new List<Object?>();
|
|
this._state = ReadState.WaitingForValue;
|
|
return;
|
|
default:
|
|
throw this.CreateParserException($"'{OpenObjectChar}' or '{OpenArrayChar}'");
|
|
}
|
|
}
|
|
|
|
private void ExtractValue() {
|
|
// determine the value based on what it starts with
|
|
switch(this._json[this._index]) {
|
|
case StringQuotedChar: // expect a string
|
|
this.ExtractStringQuoted();
|
|
break;
|
|
|
|
case OpenObjectChar: // expect object
|
|
case OpenArrayChar: // expect array
|
|
this.ExtractObject();
|
|
break;
|
|
|
|
case 't': // expect true
|
|
this.ExtractConstant(TrueLiteral, true);
|
|
break;
|
|
|
|
case 'f': // expect false
|
|
this.ExtractConstant(FalseLiteral, false);
|
|
break;
|
|
|
|
case 'n': // expect null
|
|
this.ExtractConstant(NullLiteral, null);
|
|
break;
|
|
|
|
default: // expect number
|
|
this.ExtractNumber();
|
|
break;
|
|
}
|
|
|
|
this._currentFieldName = null;
|
|
this._state = ReadState.WaitingForNextOrRootClose;
|
|
}
|
|
|
|
private static String Unescape(String str) {
|
|
// check if we need to unescape at all
|
|
if(str.IndexOf(StringEscapeChar) < 0) {
|
|
return str;
|
|
}
|
|
|
|
StringBuilder builder = new StringBuilder(str.Length);
|
|
for(Int32 i = 0; i < str.Length; i++) {
|
|
if(str[i] != StringEscapeChar) {
|
|
_ = builder.Append(str[i]);
|
|
continue;
|
|
}
|
|
|
|
if(i + 1 > str.Length - 1) {
|
|
break;
|
|
}
|
|
|
|
// escape sequence begins here
|
|
switch(str[i + 1]) {
|
|
case 'u':
|
|
i = ExtractEscapeSequence(str, i, builder);
|
|
break;
|
|
case 'b':
|
|
_ = builder.Append('\b');
|
|
i += 1;
|
|
break;
|
|
case 't':
|
|
_ = builder.Append('\t');
|
|
i += 1;
|
|
break;
|
|
case 'n':
|
|
_ = builder.Append('\n');
|
|
i += 1;
|
|
break;
|
|
case 'f':
|
|
_ = builder.Append('\f');
|
|
i += 1;
|
|
break;
|
|
case 'r':
|
|
_ = builder.Append('\r');
|
|
i += 1;
|
|
break;
|
|
default:
|
|
_ = builder.Append(str[i + 1]);
|
|
i += 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return builder.ToString();
|
|
}
|
|
|
|
private static Int32 ExtractEscapeSequence(String str, Int32 i, StringBuilder builder) {
|
|
Int32 startIndex = i + 2;
|
|
Int32 endIndex = i + 5;
|
|
if(endIndex > str.Length - 1) {
|
|
_ = builder.Append(str[i + 1]);
|
|
i += 1;
|
|
return i;
|
|
}
|
|
|
|
Byte[] hexCode = str.Slice(startIndex, endIndex).ConvertHexadecimalToBytes();
|
|
_ = builder.Append(Encoding.BigEndianUnicode.GetChars(hexCode));
|
|
i += 5;
|
|
return i;
|
|
}
|
|
|
|
private Int32 GetFieldNameCount() {
|
|
Int32 charCount = 0;
|
|
for(Int32 j = this._index + 1; j < this._json.Length; j++) {
|
|
if(this._json[j] == StringQuotedChar && this._json[j - 1] != StringEscapeChar) {
|
|
break;
|
|
}
|
|
|
|
charCount++;
|
|
}
|
|
|
|
return charCount;
|
|
}
|
|
|
|
private void ExtractObject() {
|
|
// Extract and set the value
|
|
Deserializer deserializer = new Deserializer(this._json, this._index);
|
|
|
|
if(this._currentFieldName != null) {
|
|
this._resultObject![this._currentFieldName] = deserializer._result!;
|
|
} else {
|
|
this._resultArray!.Add(deserializer._result!);
|
|
}
|
|
|
|
this._index = deserializer._index;
|
|
}
|
|
|
|
private void ExtractNumber() {
|
|
Int32 charCount = 0;
|
|
for(Int32 j = this._index; j < this._json.Length; j++) {
|
|
if(Char.IsWhiteSpace(this._json[j]) || this._json[j] == FieldSeparatorChar || this._resultObject != null && this._json[j] == CloseObjectChar || this._resultArray != null && this._json[j] == CloseArrayChar) {
|
|
break;
|
|
}
|
|
|
|
charCount++;
|
|
}
|
|
|
|
// Extract and set the value
|
|
String stringValue = this._json.SliceLength(this._index, charCount);
|
|
|
|
if(Decimal.TryParse(stringValue, System.Globalization.NumberStyles.Number, System.Globalization.CultureInfo.InvariantCulture, out Decimal value) == false) {
|
|
throw this.CreateParserException("[number]");
|
|
}
|
|
|
|
if(this._currentFieldName != null) {
|
|
this._resultObject![this._currentFieldName] = value;
|
|
} else {
|
|
this._resultArray!.Add(value);
|
|
}
|
|
|
|
this._index += charCount - 1;
|
|
}
|
|
|
|
private void ExtractConstant(String boolValue, Boolean? value) {
|
|
if(this._json.SliceLength(this._index, boolValue.Length) != boolValue) {
|
|
throw this.CreateParserException($"'{ValueSeparatorChar}'");
|
|
}
|
|
|
|
// Extract and set the value
|
|
if(this._currentFieldName != null) {
|
|
this._resultObject![this._currentFieldName] = value;
|
|
} else {
|
|
this._resultArray!.Add(value);
|
|
}
|
|
|
|
this._index += boolValue.Length - 1;
|
|
}
|
|
|
|
private void ExtractStringQuoted() {
|
|
Int32 charCount = 0;
|
|
Boolean escapeCharFound = false;
|
|
for(Int32 j = this._index + 1; j < this._json.Length; j++) {
|
|
if(this._json[j] == StringQuotedChar && !escapeCharFound) {
|
|
break;
|
|
}
|
|
|
|
escapeCharFound = this._json[j] == StringEscapeChar && !escapeCharFound;
|
|
charCount++;
|
|
}
|
|
|
|
// Extract and set the value
|
|
String value = Unescape(this._json.SliceLength(this._index + 1, charCount));
|
|
if(this._currentFieldName != null) {
|
|
this._resultObject![this._currentFieldName] = value;
|
|
} else {
|
|
this._resultArray!.Add(value);
|
|
}
|
|
|
|
this._index += charCount + 1;
|
|
}
|
|
|
|
private FormatException CreateParserException(String expected) {
|
|
Tuple<Int32, Int32> textPosition = this._json.TextPositionAt(this._index);
|
|
return new FormatException($"Parser error (Line {textPosition.Item1}, Col {textPosition.Item2}, State {this._state}): Expected {expected} but got '{this._json[this._index]}'.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Defines the different JSON read states.
|
|
/// </summary>
|
|
private enum ReadState {
|
|
WaitingForRootOpen,
|
|
WaitingForField,
|
|
WaitingForColon,
|
|
WaitingForValue,
|
|
WaitingForNextOrRootClose,
|
|
}
|
|
}
|
|
}
|
|
}
|