813 lines
36 KiB
C#
813 lines
36 KiB
C#
#region Copyright notice and license
|
|
// Protocol Buffers - Google's data interchange format
|
|
// Copyright 2008 Google Inc. All rights reserved.
|
|
//
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file or at
|
|
// https://developers.google.com/open-source/licenses/bsd
|
|
#endregion
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Text;
|
|
|
|
namespace 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();
|
|
switch (next)
|
|
{
|
|
case -1:
|
|
ValidateState(State.ExpectedEndOfDocument, "Unexpected end of document in state: ");
|
|
state = State.ReaderExhausted;
|
|
return JsonToken.EndDocument;
|
|
|
|
// 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((char) next);
|
|
ValidateAndModifyStateForValue("Invalid state to read a number token: ");
|
|
return JsonToken.Value(number);
|
|
default:
|
|
throw new InvalidJsonException($"Invalid first character of token: {(char) next}");
|
|
}
|
|
}
|
|
}
|
|
|
|
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()
|
|
{
|
|
//builder will not be released in case of an exception, but this is not a problem and we will create new on next Acquire
|
|
var builder = StringBuilderCache.Acquire();
|
|
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 StringBuilderCache.GetStringAndRelease(builder);
|
|
}
|
|
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);
|
|
builder.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");
|
|
return c switch
|
|
{
|
|
'n' => '\n',
|
|
'\\' => '\\',
|
|
'b' => '\b',
|
|
'f' => '\f',
|
|
'r' => '\r',
|
|
't' => '\t',
|
|
'"' => '"',
|
|
'/' => '/',
|
|
'u' => ReadUnicodeEscape(),
|
|
_ => 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++)
|
|
{
|
|
int next = reader.Read();
|
|
if (next != text[i])
|
|
{
|
|
// Only check for "end of text" when we've detected that the character differs from the
|
|
// expected one.
|
|
var message = next == -1
|
|
? $"Unexpected end of text while reading literal token {text}"
|
|
: $"Unexpected character while reading literal token {text}";
|
|
throw reader.CreateException(message);
|
|
}
|
|
}
|
|
}
|
|
|
|
private double ReadNumber(char initialCharacter)
|
|
{
|
|
//builder will not be released in case of an exception, but this is not a problem and we will create new on next Acquire
|
|
var builder = StringBuilderCache.Acquire();
|
|
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".
|
|
int 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 != -1)
|
|
{
|
|
reader.PushBack((char) next);
|
|
}
|
|
|
|
// TODO: What exception should we throw if the value can't be represented as a double?
|
|
var builderValue = StringBuilderCache.GetStringAndRelease(builder);
|
|
try
|
|
{
|
|
double result = double.Parse(builderValue,
|
|
NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent,
|
|
CultureInfo.InvariantCulture);
|
|
|
|
// .NET Core 3.0 and later returns infinity if the number is too large or small to be represented.
|
|
// For compatibility with other Protobuf implementations the tokenizer should still throw.
|
|
if (double.IsInfinity(result))
|
|
{
|
|
throw reader.CreateException("Numeric value out of range: " + builderValue);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
catch (OverflowException)
|
|
{
|
|
throw reader.CreateException("Numeric value out of range: " + builderValue);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Copies an integer into a StringBuilder.
|
|
/// </summary>
|
|
/// <param name="builder">The builder to read the number into</param>
|
|
/// <returns>The character following the integer, or -1 for end-of-text.</returns>
|
|
private int 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 next = ConsumeDigits(builder, out int digitCount);
|
|
if (first == '0' && digitCount != 0)
|
|
{
|
|
throw reader.CreateException("Invalid numeric literal: leading 0 for non-zero value.");
|
|
}
|
|
return next;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Copies the fractional part of an integer into a StringBuilder, assuming reader is positioned after a period.
|
|
/// </summary>
|
|
/// <param name="builder">The builder to read the number into</param>
|
|
/// <returns>The character following the fractional part, or -1 for end-of-text.</returns>
|
|
private int ReadFrac(StringBuilder builder)
|
|
{
|
|
builder.Append('.'); // Already consumed this
|
|
int next = ConsumeDigits(builder, out int digitCount);
|
|
if (digitCount == 0)
|
|
{
|
|
throw reader.CreateException("Invalid numeric literal: fraction with no trailing digits");
|
|
}
|
|
return next;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Copies the exponent part of a number into a StringBuilder, with an assumption that the reader is already positioned after the "e".
|
|
/// </summary>
|
|
/// <param name="builder">The builder to read the number into</param>
|
|
/// <returns>The character following the exponent, or -1 for end-of-text.</returns>
|
|
private int ReadExp(StringBuilder builder)
|
|
{
|
|
builder.Append('E'); // Already consumed this (or 'e')
|
|
int next = reader.Read();
|
|
if (next == -1)
|
|
{
|
|
throw reader.CreateException("Invalid numeric literal: exponent with no trailing digits");
|
|
}
|
|
if (next == '-' || next == '+')
|
|
{
|
|
builder.Append((char) next);
|
|
}
|
|
else
|
|
{
|
|
reader.PushBack((char) next);
|
|
}
|
|
next = ConsumeDigits(builder, out int digitCount);
|
|
if (digitCount == 0)
|
|
{
|
|
throw reader.CreateException("Invalid numeric literal: exponent without value");
|
|
}
|
|
return next;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Copies a sequence of digits into a StringBuilder.
|
|
/// </summary>
|
|
/// <param name="builder">The builder to read the number into</param>
|
|
/// <param name="count">The number of digits appended to the builder</param>
|
|
/// <returns>The character following the digits, or -1 for end-of-text.</returns>
|
|
private int ConsumeDigits(StringBuilder builder, out int count)
|
|
{
|
|
count = 0;
|
|
while (true)
|
|
{
|
|
int next = reader.Read();
|
|
if (next == -1 || next < '0' || next > '9')
|
|
{
|
|
return next;
|
|
}
|
|
count++;
|
|
builder.Append((char) next);
|
|
}
|
|
}
|
|
|
|
/// <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();
|
|
state = parent switch
|
|
{
|
|
ContainerType.Object => State.ObjectAfterProperty,
|
|
ContainerType.Array => State.ArrayAfterValue,
|
|
ContainerType.Document => State.ExpectedEndOfDocument,
|
|
_ => 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, or -1 if there is no buffered character.
|
|
/// </summary>
|
|
private int nextChar = -1;
|
|
|
|
/// <summary>
|
|
/// Returns the next character in the stream, or -1 if we have reached the end of the stream.
|
|
/// </summary>
|
|
internal int Read()
|
|
{
|
|
if (nextChar != -1)
|
|
{
|
|
int tmp = nextChar;
|
|
nextChar = -1;
|
|
return tmp;
|
|
}
|
|
return reader.Read();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads the next character from the underlying reader, throwing an <see cref="InvalidJsonException" />
|
|
/// with the specified message if there are no more characters available.
|
|
/// </summary>
|
|
internal char ReadOrFail(string messageOnFailure)
|
|
{
|
|
int next = Read();
|
|
if (next == -1)
|
|
{
|
|
throw CreateException(messageOnFailure);
|
|
}
|
|
return (char) next;
|
|
}
|
|
|
|
internal void PushBack(char c)
|
|
{
|
|
if (nextChar != -1)
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Provide a cached reusable instance of stringbuilder per thread.
|
|
/// Copied from https://github.com/dotnet/runtime/blob/main/src/libraries/Common/src/System/Text/StringBuilderCache.cs
|
|
/// </summary>
|
|
private static class StringBuilderCache
|
|
{
|
|
private const int MaxCachedStringBuilderSize = 360;
|
|
private const int DefaultStringBuilderCapacity = 16; // == StringBuilder.DefaultCapacity
|
|
|
|
[ThreadStatic]
|
|
private static StringBuilder cachedInstance;
|
|
|
|
/// <summary>Get a StringBuilder for the specified capacity.</summary>
|
|
/// <remarks>If a StringBuilder of an appropriate size is cached, it will be returned and the cache emptied.</remarks>
|
|
public static StringBuilder Acquire(int capacity = DefaultStringBuilderCapacity)
|
|
{
|
|
if (capacity <= MaxCachedStringBuilderSize)
|
|
{
|
|
StringBuilder sb = cachedInstance;
|
|
if (sb != null)
|
|
{
|
|
// Avoid stringbuilder block fragmentation by getting a new StringBuilder
|
|
// when the requested size is larger than the current capacity
|
|
if (capacity <= sb.Capacity)
|
|
{
|
|
cachedInstance = null;
|
|
sb.Clear();
|
|
return sb;
|
|
}
|
|
}
|
|
}
|
|
|
|
return new StringBuilder(capacity);
|
|
}
|
|
|
|
/// <summary>Place the specified builder in the cache if it is not too big.</summary>
|
|
private static void Release(StringBuilder sb)
|
|
{
|
|
if (sb.Capacity <= MaxCachedStringBuilderSize)
|
|
{
|
|
cachedInstance = cachedInstance?.Capacity >= sb.Capacity ? cachedInstance : sb;
|
|
}
|
|
}
|
|
|
|
/// <summary>ToString() the stringbuilder, Release it to the cache, and return the resulting string.</summary>
|
|
public static string GetStringAndRelease(StringBuilder sb)
|
|
{
|
|
string result = sb.ToString();
|
|
Release(sb);
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|