// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. namespace LightJson.Serialization { using System; using System.Globalization; using System.IO; using System.Text; using ErrorType = JsonParseException.ErrorType; /// /// Represents a reader that can read JsonValues. /// internal sealed class JsonReader { private TextScanner scanner; private JsonReader(TextReader reader) { this.scanner = new TextScanner(reader); } /// /// Creates a JsonValue by using the given TextReader. /// /// The TextReader used to read a JSON message. /// The parsed . public static JsonValue Parse(TextReader reader) { if (reader == null) { throw new ArgumentNullException(nameof(reader)); } return new JsonReader(reader).Parse(); } /// /// Creates a JsonValue by reader the JSON message in the given string. /// /// The string containing the JSON message. /// The parsed . public static JsonValue Parse(string source) { if (source == null) { throw new ArgumentNullException(nameof(source)); } using (var reader = new StringReader(source)) { return Parse(reader); } } private string ReadJsonKey() { return this.ReadString(); } private JsonValue ReadJsonValue() { this.scanner.SkipWhitespace(); var next = this.scanner.Peek(); if (char.IsNumber(next)) { return this.ReadNumber(); } switch (next) { case '{': return this.ReadObject(); case '[': return this.ReadArray(); case '"': return this.ReadString(); case '-': return this.ReadNumber(); case 't': case 'f': return this.ReadBoolean(); case 'n': return this.ReadNull(); default: throw new JsonParseException( ErrorType.InvalidOrUnexpectedCharacter, this.scanner.Position); } } private JsonValue ReadNull() { this.scanner.Assert("null"); return JsonValue.Null; } private JsonValue ReadBoolean() { switch (this.scanner.Peek()) { case 't': this.scanner.Assert("true"); return true; default: this.scanner.Assert("false"); return false; } } private void ReadDigits(StringBuilder builder) { while (true) { int next = this.scanner.Peek(throwAtEndOfFile: false); if (next == -1 || !char.IsNumber((char)next)) { return; } builder.Append(this.scanner.Read()); } } private JsonValue ReadNumber() { var builder = new StringBuilder(); if (this.scanner.Peek() == '-') { builder.Append(this.scanner.Read()); } if (this.scanner.Peek() == '0') { builder.Append(this.scanner.Read()); } else { this.ReadDigits(builder); } if (this.scanner.Peek(throwAtEndOfFile: false) == '.') { builder.Append(this.scanner.Read()); this.ReadDigits(builder); } if (this.scanner.Peek(throwAtEndOfFile: false) == 'e' || this.scanner.Peek(throwAtEndOfFile: false) == 'E') { builder.Append(this.scanner.Read()); var next = this.scanner.Peek(); switch (next) { case '+': case '-': builder.Append(this.scanner.Read()); break; } this.ReadDigits(builder); } return double.Parse( builder.ToString(), CultureInfo.InvariantCulture); } private string ReadString() { var builder = new StringBuilder(); this.scanner.Assert('"'); while (true) { var errorPosition = this.scanner.Position; var c = this.scanner.Read(); if (c == '\\') { errorPosition = this.scanner.Position; c = this.scanner.Read(); switch (char.ToLower(c)) { case '"': case '\\': case '/': builder.Append(c); break; case 'b': builder.Append('\b'); break; case 'f': builder.Append('\f'); break; case 'n': builder.Append('\n'); break; case 'r': builder.Append('\r'); break; case 't': builder.Append('\t'); break; case 'u': builder.Append(this.ReadUnicodeLiteral()); break; default: throw new JsonParseException( ErrorType.InvalidOrUnexpectedCharacter, errorPosition); } } else if (c == '"') { break; } else { if (char.IsControl(c)) { throw new JsonParseException( ErrorType.InvalidOrUnexpectedCharacter, errorPosition); } else { builder.Append(c); } } } return builder.ToString(); } private int ReadHexDigit() { var errorPosition = this.scanner.Position; switch (char.ToUpper(this.scanner.Read())) { case '0': return 0; case '1': return 1; case '2': return 2; case '3': return 3; case '4': return 4; case '5': return 5; case '6': return 6; case '7': return 7; case '8': return 8; case '9': return 9; case 'A': return 10; case 'B': return 11; case 'C': return 12; case 'D': return 13; case 'E': return 14; case 'F': return 15; default: throw new JsonParseException( ErrorType.InvalidOrUnexpectedCharacter, errorPosition); } } private char ReadUnicodeLiteral() { int value = 0; value += this.ReadHexDigit() * 4096; // 16^3 value += this.ReadHexDigit() * 256; // 16^2 value += this.ReadHexDigit() * 16; // 16^1 value += this.ReadHexDigit(); // 16^0 return (char)value; } private JsonObject ReadObject() { return this.ReadObject(new JsonObject()); } private JsonObject ReadObject(JsonObject jsonObject) { this.scanner.Assert('{'); this.scanner.SkipWhitespace(); if (this.scanner.Peek() == '}') { this.scanner.Read(); } else { while (true) { this.scanner.SkipWhitespace(); var errorPosition = this.scanner.Position; var key = this.ReadJsonKey(); if (jsonObject.ContainsKey(key)) { throw new JsonParseException( ErrorType.DuplicateObjectKeys, errorPosition); } this.scanner.SkipWhitespace(); this.scanner.Assert(':'); this.scanner.SkipWhitespace(); var value = this.ReadJsonValue(); jsonObject.Add(key, value); this.scanner.SkipWhitespace(); errorPosition = this.scanner.Position; var next = this.scanner.Read(); if (next == ',') { // Allow trailing commas in objects this.scanner.SkipWhitespace(); if (this.scanner.Peek() == '}') { next = this.scanner.Read(); } } if (next == '}') { break; } else if (next == ',') { continue; } else { throw new JsonParseException( ErrorType.InvalidOrUnexpectedCharacter, errorPosition); } } } return jsonObject; } private JsonArray ReadArray() { return this.ReadArray(new JsonArray()); } private JsonArray ReadArray(JsonArray jsonArray) { this.scanner.Assert('['); this.scanner.SkipWhitespace(); if (this.scanner.Peek() == ']') { this.scanner.Read(); } else { while (true) { this.scanner.SkipWhitespace(); var value = this.ReadJsonValue(); jsonArray.Add(value); this.scanner.SkipWhitespace(); var errorPosition = this.scanner.Position; var next = this.scanner.Read(); if (next == ',') { // Allow trailing commas in arrays this.scanner.SkipWhitespace(); if (this.scanner.Peek() == ']') { next = this.scanner.Read(); } } if (next == ']') { break; } else if (next == ',') { continue; } else { throw new JsonParseException( ErrorType.InvalidOrUnexpectedCharacter, errorPosition); } } } return jsonArray; } private JsonValue Parse() { this.scanner.SkipWhitespace(); return this.ReadJsonValue(); } } }