767 lines
33 KiB
C#
767 lines
33 KiB
C#
|
#region Copyright notice and license
|
|||
|
// Protocol Buffers - Google's data interchange format
|
|||
|
// Copyright 2008 Google Inc. All rights reserved.
|
|||
|
// https://developers.google.com/protocol-buffers/
|
|||
|
//
|
|||
|
// Redistribution and use in source and binary forms, with or without
|
|||
|
// modification, are permitted provided that the following conditions are
|
|||
|
// met:
|
|||
|
//
|
|||
|
// * Redistributions of source code must retain the above copyright
|
|||
|
// notice, this list of conditions and the following disclaimer.
|
|||
|
// * Redistributions in binary form must reproduce the above
|
|||
|
// copyright notice, this list of conditions and the following disclaimer
|
|||
|
// in the documentation and/or other materials provided with the
|
|||
|
// distribution.
|
|||
|
// * Neither the name of Google Inc. nor the names of its
|
|||
|
// contributors may be used to endorse or promote products derived from
|
|||
|
// this software without specific prior written permission.
|
|||
|
//
|
|||
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|||
|
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|||
|
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|||
|
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|||
|
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|||
|
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|||
|
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|||
|
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|||
|
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|||
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|||
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
|
#endregion
|
|||
|
using System;
|
|||
|
using System.Collections.Generic;
|
|||
|
using System.Globalization;
|
|||
|
using System.IO;
|
|||
|
using System.Text;
|
|||
|
|
|||
|
namespace LC.Google.Protobuf
|
|||
|
{
|
|||
|
/// <summary>
|
|||
|
/// Simple but strict JSON tokenizer, rigidly following RFC 7159.
|
|||
|
/// </summary>
|
|||
|
/// <remarks>
|
|||
|
/// <para>
|
|||
|
/// This tokenizer is stateful, and only returns "useful" tokens - names, values etc.
|
|||
|
/// It does not create tokens for the separator between names and values, or for the comma
|
|||
|
/// between values. It validates the token stream as it goes - so callers can assume that the
|
|||
|
/// tokens it produces are appropriate. For example, it would never produce "start object, end array."
|
|||
|
/// </para>
|
|||
|
/// <para>Implementation details: the base class handles single token push-back and </para>
|
|||
|
/// <para>Not thread-safe.</para>
|
|||
|
/// </remarks>
|
|||
|
internal abstract class JsonTokenizer
|
|||
|
{
|
|||
|
private JsonToken bufferedToken;
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Creates a tokenizer that reads from the given text reader.
|
|||
|
/// </summary>
|
|||
|
internal static JsonTokenizer FromTextReader(TextReader reader)
|
|||
|
{
|
|||
|
return new JsonTextTokenizer(reader);
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Creates a tokenizer that first replays the given list of tokens, then continues reading
|
|||
|
/// from another tokenizer. Note that if the returned tokenizer is "pushed back", that does not push back
|
|||
|
/// on the continuation tokenizer, or vice versa. Care should be taken when using this method - it was
|
|||
|
/// created for the sake of Any parsing.
|
|||
|
/// </summary>
|
|||
|
internal static JsonTokenizer FromReplayedTokens(IList<JsonToken> tokens, JsonTokenizer continuation)
|
|||
|
{
|
|||
|
return new JsonReplayTokenizer(tokens, continuation);
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Returns the depth of the stack, purely in objects (not collections).
|
|||
|
/// Informally, this is the number of remaining unclosed '{' characters we have.
|
|||
|
/// </summary>
|
|||
|
internal int ObjectDepth { get; private set; }
|
|||
|
|
|||
|
// TODO: Why do we allow a different token to be pushed back? It might be better to always remember the previous
|
|||
|
// token returned, and allow a parameterless Rewind() method (which could only be called once, just like the current PushBack).
|
|||
|
internal void PushBack(JsonToken token)
|
|||
|
{
|
|||
|
if (bufferedToken != null)
|
|||
|
{
|
|||
|
throw new InvalidOperationException("Can't push back twice");
|
|||
|
}
|
|||
|
bufferedToken = token;
|
|||
|
if (token.Type == JsonToken.TokenType.StartObject)
|
|||
|
{
|
|||
|
ObjectDepth--;
|
|||
|
}
|
|||
|
else if (token.Type == JsonToken.TokenType.EndObject)
|
|||
|
{
|
|||
|
ObjectDepth++;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Returns the next JSON token in the stream. An EndDocument token is returned to indicate the end of the stream,
|
|||
|
/// after which point <c>Next()</c> should not be called again.
|
|||
|
/// </summary>
|
|||
|
/// <remarks>This implementation provides single-token buffering, and calls <see cref="NextImpl"/> if there is no buffered token.</remarks>
|
|||
|
/// <returns>The next token in the stream. This is never null.</returns>
|
|||
|
/// <exception cref="InvalidOperationException">This method is called after an EndDocument token has been returned</exception>
|
|||
|
/// <exception cref="InvalidJsonException">The input text does not comply with RFC 7159</exception>
|
|||
|
internal JsonToken Next()
|
|||
|
{
|
|||
|
JsonToken tokenToReturn;
|
|||
|
if (bufferedToken != null)
|
|||
|
{
|
|||
|
tokenToReturn = bufferedToken;
|
|||
|
bufferedToken = null;
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
tokenToReturn = NextImpl();
|
|||
|
}
|
|||
|
if (tokenToReturn.Type == JsonToken.TokenType.StartObject)
|
|||
|
{
|
|||
|
ObjectDepth++;
|
|||
|
}
|
|||
|
else if (tokenToReturn.Type == JsonToken.TokenType.EndObject)
|
|||
|
{
|
|||
|
ObjectDepth--;
|
|||
|
}
|
|||
|
return tokenToReturn;
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Returns the next JSON token in the stream, when requested by the base class. (The <see cref="Next"/> method delegates
|
|||
|
/// to this if it doesn't have a buffered token.)
|
|||
|
/// </summary>
|
|||
|
/// <exception cref="InvalidOperationException">This method is called after an EndDocument token has been returned</exception>
|
|||
|
/// <exception cref="InvalidJsonException">The input text does not comply with RFC 7159</exception>
|
|||
|
protected abstract JsonToken NextImpl();
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Skips the value we're about to read. This must only be called immediately after reading a property name.
|
|||
|
/// If the value is an object or an array, the complete object/array is skipped.
|
|||
|
/// </summary>
|
|||
|
internal void SkipValue()
|
|||
|
{
|
|||
|
// We'll assume that Next() makes sure that the end objects and end arrays are all valid.
|
|||
|
// All we care about is the total nesting depth we need to close.
|
|||
|
int depth = 0;
|
|||
|
|
|||
|
// do/while rather than while loop so that we read at least one token.
|
|||
|
do
|
|||
|
{
|
|||
|
var token = Next();
|
|||
|
switch (token.Type)
|
|||
|
{
|
|||
|
case JsonToken.TokenType.EndArray:
|
|||
|
case JsonToken.TokenType.EndObject:
|
|||
|
depth--;
|
|||
|
break;
|
|||
|
case JsonToken.TokenType.StartArray:
|
|||
|
case JsonToken.TokenType.StartObject:
|
|||
|
depth++;
|
|||
|
break;
|
|||
|
}
|
|||
|
} while (depth != 0);
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Tokenizer which first exhausts a list of tokens, then consults another tokenizer.
|
|||
|
/// </summary>
|
|||
|
private class JsonReplayTokenizer : JsonTokenizer
|
|||
|
{
|
|||
|
private readonly IList<JsonToken> tokens;
|
|||
|
private readonly JsonTokenizer nextTokenizer;
|
|||
|
private int nextTokenIndex;
|
|||
|
|
|||
|
internal JsonReplayTokenizer(IList<JsonToken> tokens, JsonTokenizer nextTokenizer)
|
|||
|
{
|
|||
|
this.tokens = tokens;
|
|||
|
this.nextTokenizer = nextTokenizer;
|
|||
|
}
|
|||
|
|
|||
|
// FIXME: Object depth not maintained...
|
|||
|
protected override JsonToken NextImpl()
|
|||
|
{
|
|||
|
if (nextTokenIndex >= tokens.Count)
|
|||
|
{
|
|||
|
return nextTokenizer.Next();
|
|||
|
}
|
|||
|
return tokens[nextTokenIndex++];
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Tokenizer which does all the *real* work of parsing JSON.
|
|||
|
/// </summary>
|
|||
|
private sealed class JsonTextTokenizer : JsonTokenizer
|
|||
|
{
|
|||
|
// The set of states in which a value is valid next token.
|
|||
|
private static readonly State ValueStates = State.ArrayStart | State.ArrayAfterComma | State.ObjectAfterColon | State.StartOfDocument;
|
|||
|
|
|||
|
private readonly Stack<ContainerType> containerStack = new Stack<ContainerType>();
|
|||
|
private readonly PushBackReader reader;
|
|||
|
private State state;
|
|||
|
|
|||
|
internal JsonTextTokenizer(TextReader reader)
|
|||
|
{
|
|||
|
this.reader = new PushBackReader(reader);
|
|||
|
state = State.StartOfDocument;
|
|||
|
containerStack.Push(ContainerType.Document);
|
|||
|
}
|
|||
|
|
|||
|
/// <remarks>
|
|||
|
/// This method essentially just loops through characters skipping whitespace, validating and
|
|||
|
/// changing state (e.g. from ObjectBeforeColon to ObjectAfterColon)
|
|||
|
/// until it reaches something which will be a genuine token (e.g. a start object, or a value) at which point
|
|||
|
/// it returns the token. Although the method is large, it would be relatively hard to break down further... most
|
|||
|
/// of it is the large switch statement, which sometimes returns and sometimes doesn't.
|
|||
|
/// </remarks>
|
|||
|
protected override JsonToken NextImpl()
|
|||
|
{
|
|||
|
if (state == State.ReaderExhausted)
|
|||
|
{
|
|||
|
throw new InvalidOperationException("Next() called after end of document");
|
|||
|
}
|
|||
|
while (true)
|
|||
|
{
|
|||
|
var next = reader.Read();
|
|||
|
if (next == null)
|
|||
|
{
|
|||
|
ValidateState(State.ExpectedEndOfDocument, "Unexpected end of document in state: ");
|
|||
|
state = State.ReaderExhausted;
|
|||
|
return JsonToken.EndDocument;
|
|||
|
}
|
|||
|
switch (next.Value)
|
|||
|
{
|
|||
|
// Skip whitespace between tokens
|
|||
|
case ' ':
|
|||
|
case '\t':
|
|||
|
case '\r':
|
|||
|
case '\n':
|
|||
|
break;
|
|||
|
case ':':
|
|||
|
ValidateState(State.ObjectBeforeColon, "Invalid state to read a colon: ");
|
|||
|
state = State.ObjectAfterColon;
|
|||
|
break;
|
|||
|
case ',':
|
|||
|
ValidateState(State.ObjectAfterProperty | State.ArrayAfterValue, "Invalid state to read a comma: ");
|
|||
|
state = state == State.ObjectAfterProperty ? State.ObjectAfterComma : State.ArrayAfterComma;
|
|||
|
break;
|
|||
|
case '"':
|
|||
|
string stringValue = ReadString();
|
|||
|
if ((state & (State.ObjectStart | State.ObjectAfterComma)) != 0)
|
|||
|
{
|
|||
|
state = State.ObjectBeforeColon;
|
|||
|
return JsonToken.Name(stringValue);
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
ValidateAndModifyStateForValue("Invalid state to read a double quote: ");
|
|||
|
return JsonToken.Value(stringValue);
|
|||
|
}
|
|||
|
case '{':
|
|||
|
ValidateState(ValueStates, "Invalid state to read an open brace: ");
|
|||
|
state = State.ObjectStart;
|
|||
|
containerStack.Push(ContainerType.Object);
|
|||
|
return JsonToken.StartObject;
|
|||
|
case '}':
|
|||
|
ValidateState(State.ObjectAfterProperty | State.ObjectStart, "Invalid state to read a close brace: ");
|
|||
|
PopContainer();
|
|||
|
return JsonToken.EndObject;
|
|||
|
case '[':
|
|||
|
ValidateState(ValueStates, "Invalid state to read an open square bracket: ");
|
|||
|
state = State.ArrayStart;
|
|||
|
containerStack.Push(ContainerType.Array);
|
|||
|
return JsonToken.StartArray;
|
|||
|
case ']':
|
|||
|
ValidateState(State.ArrayAfterValue | State.ArrayStart, "Invalid state to read a close square bracket: ");
|
|||
|
PopContainer();
|
|||
|
return JsonToken.EndArray;
|
|||
|
case 'n': // Start of null
|
|||
|
ConsumeLiteral("null");
|
|||
|
ValidateAndModifyStateForValue("Invalid state to read a null literal: ");
|
|||
|
return JsonToken.Null;
|
|||
|
case 't': // Start of true
|
|||
|
ConsumeLiteral("true");
|
|||
|
ValidateAndModifyStateForValue("Invalid state to read a true literal: ");
|
|||
|
return JsonToken.True;
|
|||
|
case 'f': // Start of false
|
|||
|
ConsumeLiteral("false");
|
|||
|
ValidateAndModifyStateForValue("Invalid state to read a false literal: ");
|
|||
|
return JsonToken.False;
|
|||
|
case '-': // Start of a number
|
|||
|
case '0':
|
|||
|
case '1':
|
|||
|
case '2':
|
|||
|
case '3':
|
|||
|
case '4':
|
|||
|
case '5':
|
|||
|
case '6':
|
|||
|
case '7':
|
|||
|
case '8':
|
|||
|
case '9':
|
|||
|
double number = ReadNumber(next.Value);
|
|||
|
ValidateAndModifyStateForValue("Invalid state to read a number token: ");
|
|||
|
return JsonToken.Value(number);
|
|||
|
default:
|
|||
|
throw new InvalidJsonException("Invalid first character of token: " + next.Value);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
private void ValidateState(State validStates, string errorPrefix)
|
|||
|
{
|
|||
|
if ((validStates & state) == 0)
|
|||
|
{
|
|||
|
throw reader.CreateException(errorPrefix + state);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Reads a string token. It is assumed that the opening " has already been read.
|
|||
|
/// </summary>
|
|||
|
private string ReadString()
|
|||
|
{
|
|||
|
var value = new StringBuilder();
|
|||
|
bool haveHighSurrogate = false;
|
|||
|
while (true)
|
|||
|
{
|
|||
|
char c = reader.ReadOrFail("Unexpected end of text while reading string");
|
|||
|
if (c < ' ')
|
|||
|
{
|
|||
|
throw reader.CreateException(string.Format(CultureInfo.InvariantCulture, "Invalid character in string literal: U+{0:x4}", (int) c));
|
|||
|
}
|
|||
|
if (c == '"')
|
|||
|
{
|
|||
|
if (haveHighSurrogate)
|
|||
|
{
|
|||
|
throw reader.CreateException("Invalid use of surrogate pair code units");
|
|||
|
}
|
|||
|
return value.ToString();
|
|||
|
}
|
|||
|
if (c == '\\')
|
|||
|
{
|
|||
|
c = ReadEscapedCharacter();
|
|||
|
}
|
|||
|
// TODO: Consider only allowing surrogate pairs that are either both escaped,
|
|||
|
// or both not escaped. It would be a very odd text stream that contained a "lone" high surrogate
|
|||
|
// followed by an escaped low surrogate or vice versa... and that couldn't even be represented in UTF-8.
|
|||
|
if (haveHighSurrogate != char.IsLowSurrogate(c))
|
|||
|
{
|
|||
|
throw reader.CreateException("Invalid use of surrogate pair code units");
|
|||
|
}
|
|||
|
haveHighSurrogate = char.IsHighSurrogate(c);
|
|||
|
value.Append(c);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Reads an escaped character. It is assumed that the leading backslash has already been read.
|
|||
|
/// </summary>
|
|||
|
private char ReadEscapedCharacter()
|
|||
|
{
|
|||
|
char c = reader.ReadOrFail("Unexpected end of text while reading character escape sequence");
|
|||
|
switch (c)
|
|||
|
{
|
|||
|
case 'n':
|
|||
|
return '\n';
|
|||
|
case '\\':
|
|||
|
return '\\';
|
|||
|
case 'b':
|
|||
|
return '\b';
|
|||
|
case 'f':
|
|||
|
return '\f';
|
|||
|
case 'r':
|
|||
|
return '\r';
|
|||
|
case 't':
|
|||
|
return '\t';
|
|||
|
case '"':
|
|||
|
return '"';
|
|||
|
case '/':
|
|||
|
return '/';
|
|||
|
case 'u':
|
|||
|
return ReadUnicodeEscape();
|
|||
|
default:
|
|||
|
throw reader.CreateException(string.Format(CultureInfo.InvariantCulture, "Invalid character in character escape sequence: U+{0:x4}", (int) c));
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Reads an escaped Unicode 4-nybble hex sequence. It is assumed that the leading \u has already been read.
|
|||
|
/// </summary>
|
|||
|
private char ReadUnicodeEscape()
|
|||
|
{
|
|||
|
int result = 0;
|
|||
|
for (int i = 0; i < 4; i++)
|
|||
|
{
|
|||
|
char c = reader.ReadOrFail("Unexpected end of text while reading Unicode escape sequence");
|
|||
|
int nybble;
|
|||
|
if (c >= '0' && c <= '9')
|
|||
|
{
|
|||
|
nybble = c - '0';
|
|||
|
}
|
|||
|
else if (c >= 'a' && c <= 'f')
|
|||
|
{
|
|||
|
nybble = c - 'a' + 10;
|
|||
|
}
|
|||
|
else if (c >= 'A' && c <= 'F')
|
|||
|
{
|
|||
|
nybble = c - 'A' + 10;
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
throw reader.CreateException(string.Format(CultureInfo.InvariantCulture, "Invalid character in character escape sequence: U+{0:x4}", (int) c));
|
|||
|
}
|
|||
|
result = (result << 4) + nybble;
|
|||
|
}
|
|||
|
return (char) result;
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Consumes a text-only literal, throwing an exception if the read text doesn't match it.
|
|||
|
/// It is assumed that the first letter of the literal has already been read.
|
|||
|
/// </summary>
|
|||
|
private void ConsumeLiteral(string text)
|
|||
|
{
|
|||
|
for (int i = 1; i < text.Length; i++)
|
|||
|
{
|
|||
|
char? next = reader.Read();
|
|||
|
if (next == null)
|
|||
|
{
|
|||
|
throw reader.CreateException("Unexpected end of text while reading literal token " + text);
|
|||
|
}
|
|||
|
if (next.Value != text[i])
|
|||
|
{
|
|||
|
throw reader.CreateException("Unexpected character while reading literal token " + text);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
private double ReadNumber(char initialCharacter)
|
|||
|
{
|
|||
|
StringBuilder builder = new StringBuilder();
|
|||
|
if (initialCharacter == '-')
|
|||
|
{
|
|||
|
builder.Append("-");
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
reader.PushBack(initialCharacter);
|
|||
|
}
|
|||
|
// Each method returns the character it read that doesn't belong in that part,
|
|||
|
// so we know what to do next, including pushing the character back at the end.
|
|||
|
// null is returned for "end of text".
|
|||
|
char? next = ReadInt(builder);
|
|||
|
if (next == '.')
|
|||
|
{
|
|||
|
next = ReadFrac(builder);
|
|||
|
}
|
|||
|
if (next == 'e' || next == 'E')
|
|||
|
{
|
|||
|
next = ReadExp(builder);
|
|||
|
}
|
|||
|
// If we read a character which wasn't part of the number, push it back so we can read it again
|
|||
|
// to parse the next token.
|
|||
|
if (next != null)
|
|||
|
{
|
|||
|
reader.PushBack(next.Value);
|
|||
|
}
|
|||
|
|
|||
|
// TODO: What exception should we throw if the value can't be represented as a double?
|
|||
|
try
|
|||
|
{
|
|||
|
return double.Parse(builder.ToString(),
|
|||
|
NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent,
|
|||
|
CultureInfo.InvariantCulture);
|
|||
|
}
|
|||
|
catch (OverflowException)
|
|||
|
{
|
|||
|
throw reader.CreateException("Numeric value out of range: " + builder);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
private char? ReadInt(StringBuilder builder)
|
|||
|
{
|
|||
|
char first = reader.ReadOrFail("Invalid numeric literal");
|
|||
|
if (first < '0' || first > '9')
|
|||
|
{
|
|||
|
throw reader.CreateException("Invalid numeric literal");
|
|||
|
}
|
|||
|
builder.Append(first);
|
|||
|
int digitCount;
|
|||
|
char? next = ConsumeDigits(builder, out digitCount);
|
|||
|
if (first == '0' && digitCount != 0)
|
|||
|
{
|
|||
|
throw reader.CreateException("Invalid numeric literal: leading 0 for non-zero value.");
|
|||
|
}
|
|||
|
return next;
|
|||
|
}
|
|||
|
|
|||
|
private char? ReadFrac(StringBuilder builder)
|
|||
|
{
|
|||
|
builder.Append('.'); // Already consumed this
|
|||
|
int digitCount;
|
|||
|
char? next = ConsumeDigits(builder, out digitCount);
|
|||
|
if (digitCount == 0)
|
|||
|
{
|
|||
|
throw reader.CreateException("Invalid numeric literal: fraction with no trailing digits");
|
|||
|
}
|
|||
|
return next;
|
|||
|
}
|
|||
|
|
|||
|
private char? ReadExp(StringBuilder builder)
|
|||
|
{
|
|||
|
builder.Append('E'); // Already consumed this (or 'e')
|
|||
|
char? next = reader.Read();
|
|||
|
if (next == null)
|
|||
|
{
|
|||
|
throw reader.CreateException("Invalid numeric literal: exponent with no trailing digits");
|
|||
|
}
|
|||
|
if (next == '-' || next == '+')
|
|||
|
{
|
|||
|
builder.Append(next.Value);
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
reader.PushBack(next.Value);
|
|||
|
}
|
|||
|
int digitCount;
|
|||
|
next = ConsumeDigits(builder, out digitCount);
|
|||
|
if (digitCount == 0)
|
|||
|
{
|
|||
|
throw reader.CreateException("Invalid numeric literal: exponent without value");
|
|||
|
}
|
|||
|
return next;
|
|||
|
}
|
|||
|
|
|||
|
private char? ConsumeDigits(StringBuilder builder, out int count)
|
|||
|
{
|
|||
|
count = 0;
|
|||
|
while (true)
|
|||
|
{
|
|||
|
char? next = reader.Read();
|
|||
|
if (next == null || next.Value < '0' || next.Value > '9')
|
|||
|
{
|
|||
|
return next;
|
|||
|
}
|
|||
|
count++;
|
|||
|
builder.Append(next.Value);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Validates that we're in a valid state to read a value (using the given error prefix if necessary)
|
|||
|
/// and changes the state to the appropriate one, e.g. ObjectAfterColon to ObjectAfterProperty.
|
|||
|
/// </summary>
|
|||
|
private void ValidateAndModifyStateForValue(string errorPrefix)
|
|||
|
{
|
|||
|
ValidateState(ValueStates, errorPrefix);
|
|||
|
switch (state)
|
|||
|
{
|
|||
|
case State.StartOfDocument:
|
|||
|
state = State.ExpectedEndOfDocument;
|
|||
|
return;
|
|||
|
case State.ObjectAfterColon:
|
|||
|
state = State.ObjectAfterProperty;
|
|||
|
return;
|
|||
|
case State.ArrayStart:
|
|||
|
case State.ArrayAfterComma:
|
|||
|
state = State.ArrayAfterValue;
|
|||
|
return;
|
|||
|
default:
|
|||
|
throw new InvalidOperationException("ValidateAndModifyStateForValue does not handle all value states (and should)");
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Pops the top-most container, and sets the state to the appropriate one for the end of a value
|
|||
|
/// in the parent container.
|
|||
|
/// </summary>
|
|||
|
private void PopContainer()
|
|||
|
{
|
|||
|
containerStack.Pop();
|
|||
|
var parent = containerStack.Peek();
|
|||
|
switch (parent)
|
|||
|
{
|
|||
|
case ContainerType.Object:
|
|||
|
state = State.ObjectAfterProperty;
|
|||
|
break;
|
|||
|
case ContainerType.Array:
|
|||
|
state = State.ArrayAfterValue;
|
|||
|
break;
|
|||
|
case ContainerType.Document:
|
|||
|
state = State.ExpectedEndOfDocument;
|
|||
|
break;
|
|||
|
default:
|
|||
|
throw new InvalidOperationException("Unexpected container type: " + parent);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
private enum ContainerType
|
|||
|
{
|
|||
|
Document, Object, Array
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Possible states of the tokenizer.
|
|||
|
/// </summary>
|
|||
|
/// <remarks>
|
|||
|
/// <para>This is a flags enum purely so we can simply and efficiently represent a set of valid states
|
|||
|
/// for checking.</para>
|
|||
|
/// <para>
|
|||
|
/// Each is documented with an example,
|
|||
|
/// where ^ represents the current position within the text stream. The examples all use string values,
|
|||
|
/// but could be any value, including nested objects/arrays.
|
|||
|
/// The complete state of the tokenizer also includes a stack to indicate the contexts (arrays/objects).
|
|||
|
/// Any additional notional state of "AfterValue" indicates that a value has been completed, at which
|
|||
|
/// point there's an immediate transition to ExpectedEndOfDocument, ObjectAfterProperty or ArrayAfterValue.
|
|||
|
/// </para>
|
|||
|
/// <para>
|
|||
|
/// These states were derived manually by reading RFC 7159 carefully.
|
|||
|
/// </para>
|
|||
|
/// </remarks>
|
|||
|
[Flags]
|
|||
|
private enum State
|
|||
|
{
|
|||
|
/// <summary>
|
|||
|
/// ^ { "foo": "bar" }
|
|||
|
/// Before the value in a document. Next states: ObjectStart, ArrayStart, "AfterValue"
|
|||
|
/// </summary>
|
|||
|
StartOfDocument = 1 << 0,
|
|||
|
/// <summary>
|
|||
|
/// { "foo": "bar" } ^
|
|||
|
/// After the value in a document. Next states: ReaderExhausted
|
|||
|
/// </summary>
|
|||
|
ExpectedEndOfDocument = 1 << 1,
|
|||
|
/// <summary>
|
|||
|
/// { "foo": "bar" } ^ (and already read to the end of the reader)
|
|||
|
/// Terminal state.
|
|||
|
/// </summary>
|
|||
|
ReaderExhausted = 1 << 2,
|
|||
|
/// <summary>
|
|||
|
/// { ^ "foo": "bar" }
|
|||
|
/// Before the *first* property in an object.
|
|||
|
/// Next states:
|
|||
|
/// "AfterValue" (empty object)
|
|||
|
/// ObjectBeforeColon (read a name)
|
|||
|
/// </summary>
|
|||
|
ObjectStart = 1 << 3,
|
|||
|
/// <summary>
|
|||
|
/// { "foo" ^ : "bar", "x": "y" }
|
|||
|
/// Next state: ObjectAfterColon
|
|||
|
/// </summary>
|
|||
|
ObjectBeforeColon = 1 << 4,
|
|||
|
/// <summary>
|
|||
|
/// { "foo" : ^ "bar", "x": "y" }
|
|||
|
/// Before any property other than the first in an object.
|
|||
|
/// (Equivalently: after any property in an object)
|
|||
|
/// Next states:
|
|||
|
/// "AfterValue" (value is simple)
|
|||
|
/// ObjectStart (value is object)
|
|||
|
/// ArrayStart (value is array)
|
|||
|
/// </summary>
|
|||
|
ObjectAfterColon = 1 << 5,
|
|||
|
/// <summary>
|
|||
|
/// { "foo" : "bar" ^ , "x" : "y" }
|
|||
|
/// At the end of a property, so expecting either a comma or end-of-object
|
|||
|
/// Next states: ObjectAfterComma or "AfterValue"
|
|||
|
/// </summary>
|
|||
|
ObjectAfterProperty = 1 << 6,
|
|||
|
/// <summary>
|
|||
|
/// { "foo":"bar", ^ "x":"y" }
|
|||
|
/// Read the comma after the previous property, so expecting another property.
|
|||
|
/// This is like ObjectStart, but closing brace isn't valid here
|
|||
|
/// Next state: ObjectBeforeColon.
|
|||
|
/// </summary>
|
|||
|
ObjectAfterComma = 1 << 7,
|
|||
|
/// <summary>
|
|||
|
/// [ ^ "foo", "bar" ]
|
|||
|
/// Before the *first* value in an array.
|
|||
|
/// Next states:
|
|||
|
/// "AfterValue" (read a value)
|
|||
|
/// "AfterValue" (end of array; will pop stack)
|
|||
|
/// </summary>
|
|||
|
ArrayStart = 1 << 8,
|
|||
|
/// <summary>
|
|||
|
/// [ "foo" ^ , "bar" ]
|
|||
|
/// After any value in an array, so expecting either a comma or end-of-array
|
|||
|
/// Next states: ArrayAfterComma or "AfterValue"
|
|||
|
/// </summary>
|
|||
|
ArrayAfterValue = 1 << 9,
|
|||
|
/// <summary>
|
|||
|
/// [ "foo", ^ "bar" ]
|
|||
|
/// After a comma in an array, so there *must* be another value (simple or complex).
|
|||
|
/// Next states: "AfterValue" (simple value), StartObject, StartArray
|
|||
|
/// </summary>
|
|||
|
ArrayAfterComma = 1 << 10
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Wrapper around a text reader allowing small amounts of buffering and location handling.
|
|||
|
/// </summary>
|
|||
|
private class PushBackReader
|
|||
|
{
|
|||
|
// TODO: Add locations for errors etc.
|
|||
|
|
|||
|
private readonly TextReader reader;
|
|||
|
|
|||
|
internal PushBackReader(TextReader reader)
|
|||
|
{
|
|||
|
// TODO: Wrap the reader in a BufferedReader?
|
|||
|
this.reader = reader;
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// The buffered next character, if we have one.
|
|||
|
/// </summary>
|
|||
|
private char? nextChar;
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Returns the next character in the stream, or null if we have reached the end.
|
|||
|
/// </summary>
|
|||
|
/// <returns></returns>
|
|||
|
internal char? Read()
|
|||
|
{
|
|||
|
if (nextChar != null)
|
|||
|
{
|
|||
|
char? tmp = nextChar;
|
|||
|
nextChar = null;
|
|||
|
return tmp;
|
|||
|
}
|
|||
|
int next = reader.Read();
|
|||
|
return next == -1 ? null : (char?) next;
|
|||
|
}
|
|||
|
|
|||
|
internal char ReadOrFail(string messageOnFailure)
|
|||
|
{
|
|||
|
char? next = Read();
|
|||
|
if (next == null)
|
|||
|
{
|
|||
|
throw CreateException(messageOnFailure);
|
|||
|
}
|
|||
|
return next.Value;
|
|||
|
}
|
|||
|
|
|||
|
internal void PushBack(char c)
|
|||
|
{
|
|||
|
if (nextChar != null)
|
|||
|
{
|
|||
|
throw new InvalidOperationException("Cannot push back when already buffering a character");
|
|||
|
}
|
|||
|
nextChar = c;
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Creates a new exception appropriate for the current state of the reader.
|
|||
|
/// </summary>
|
|||
|
internal InvalidJsonException CreateException(string message)
|
|||
|
{
|
|||
|
// TODO: Keep track of and use the location.
|
|||
|
return new InvalidJsonException(message);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|