#nullable enable using System; using System.Collections.Generic; using System.Text; namespace Swan.Formatters { /// /// 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. /// public partial class Json { /// /// A simple JSON Deserializer. /// private class Deserializer { #region State Variables private readonly Object? _result; private readonly String _json; private Dictionary? _resultObject; private List? _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(); this._state = ReadState.WaitingForField; return; case OpenArrayChar: this._resultArray = new List(); 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 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]}'."); } /// /// Defines the different JSON read states. /// private enum ReadState { WaitingForRootOpen, WaitingForField, WaitingForColon, WaitingForValue, WaitingForNextOrRootClose, } } } }