src/Elastic.Apm/Libraries/Newtonsoft.Json/JsonTextReader.cs (1,950 lines of code) (raw):

#region License // Copyright (c) 2007 James Newton-King // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation // files (the "Software"), to deal in the Software without // restriction, including without limitation the rights to use, // copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following // conditions: // // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. #endregion using System; using System.Globalization; using System.IO; using System.Runtime.CompilerServices; using Elastic.Apm.Libraries.Newtonsoft.Json.Utilities; #if HAVE_BIG_INTEGER using System.Numerics; #endif #nullable enable namespace Elastic.Apm.Libraries.Newtonsoft.Json { internal enum ReadType { Read, ReadAsInt32, ReadAsInt64, ReadAsBytes, ReadAsString, ReadAsDecimal, ReadAsDateTime, #if HAVE_DATE_TIME_OFFSET ReadAsDateTimeOffset, #endif ReadAsDouble, ReadAsBoolean } /// <summary> /// Represents a reader that provides fast, non-cached, forward-only access to JSON text data. /// </summary> internal partial class JsonTextReader : JsonReader, IJsonLineInfo { private const char UnicodeReplacementChar = '\uFFFD'; #if HAVE_BIG_INTEGER private const int MaximumJavascriptIntegerCharacterLength = 380; #endif #if DEBUG internal int LargeBufferLength { get; set; } = int.MaxValue / 2; #else private const int LargeBufferLength = int.MaxValue / 2; #endif private readonly TextReader _reader; private int _charsUsed; private int _lineStartPos; private int _lineNumber; private bool _isEndOfFile; private StringBuffer _stringBuffer; private StringReference _stringReference; private IArrayPool<char>? _arrayPool; /// <summary> /// Initializes a new instance of the <see cref="JsonTextReader" /> class with the specified <see cref="TextReader" />. /// </summary> /// <param name="reader">The <see cref="TextReader" /> containing the JSON data to read.</param> public JsonTextReader(TextReader reader) { if (reader == null) throw new ArgumentNullException(nameof(reader)); _reader = reader; _lineNumber = 1; #if HAVE_ASYNC _safeAsync = GetType() == typeof(JsonTextReader); #endif } internal char[]? CharBuffer { get; set; } internal int CharPos { get; private set; } /// <summary> /// Gets or sets the reader's property name table. /// </summary> public JsonNameTable? PropertyNameTable { get; set; } /// <summary> /// Gets or sets the reader's character buffer pool. /// </summary> public IArrayPool<char>? ArrayPool { get => _arrayPool; set { if (value == null) throw new ArgumentNullException(nameof(value)); _arrayPool = value; } } private void EnsureBufferNotEmpty() { if (_stringBuffer.IsEmpty) _stringBuffer = new StringBuffer(_arrayPool, 1024); } private void SetNewLine(bool hasNextChar) { MiscellaneousUtils.Assert(CharBuffer != null); if (hasNextChar && CharBuffer[CharPos] == StringUtils.LineFeed) CharPos++; OnNewLine(CharPos); } private void OnNewLine(int pos) { _lineNumber++; _lineStartPos = pos; } private void ParseString(char quote, ReadType readType) { CharPos++; ShiftBufferIfNeeded(); ReadStringIntoBuffer(quote); ParseReadString(quote, readType); } private void ParseReadString(char quote, ReadType readType) { SetPostValueState(true); switch (readType) { case ReadType.ReadAsBytes: Guid g; byte[] data; if (_stringReference.Length == 0) data = CollectionUtils.ArrayEmpty<byte>(); else if (_stringReference.Length == 36 && ConvertUtils.TryConvertGuid(_stringReference.ToString(), out g)) data = g.ToByteArray(); else data = Convert.FromBase64CharArray(_stringReference.Chars, _stringReference.StartIndex, _stringReference.Length); SetToken(JsonToken.Bytes, data, false); break; case ReadType.ReadAsString: var text = _stringReference.ToString(); SetToken(JsonToken.String, text, false); _quoteChar = quote; break; case ReadType.ReadAsInt32: case ReadType.ReadAsDecimal: case ReadType.ReadAsBoolean: // caller will convert result break; default: if (_dateParseHandling != DateParseHandling.None) { DateParseHandling dateParseHandling; if (readType == ReadType.ReadAsDateTime) dateParseHandling = DateParseHandling.DateTime; #if HAVE_DATE_TIME_OFFSET else if (readType == ReadType.ReadAsDateTimeOffset) { dateParseHandling = DateParseHandling.DateTimeOffset; } #endif else dateParseHandling = _dateParseHandling; if (dateParseHandling == DateParseHandling.DateTime) { if (DateTimeUtils.TryParseDateTime(_stringReference, DateTimeZoneHandling, DateFormatString, Culture, out var dt)) { SetToken(JsonToken.Date, dt, false); return; } } #if HAVE_DATE_TIME_OFFSET else { if (DateTimeUtils.TryParseDateTimeOffset(_stringReference, DateFormatString, Culture, out DateTimeOffset dt)) { SetToken(JsonToken.Date, dt, false); return; } } #endif } SetToken(JsonToken.String, _stringReference.ToString(), false); _quoteChar = quote; break; } } private static void BlockCopyChars(char[] src, int srcOffset, char[] dst, int dstOffset, int count) { const int charByteCount = 2; Buffer.BlockCopy(src, srcOffset * charByteCount, dst, dstOffset * charByteCount, count * charByteCount); } private void ShiftBufferIfNeeded() { MiscellaneousUtils.Assert(CharBuffer != null); // once in the last 10% of the buffer, or buffer is already very large then // shift the remaining content to the start to avoid unnecessarily increasing // the buffer size when reading numbers/strings var length = CharBuffer.Length; if (length - CharPos <= length * 0.1 || length >= LargeBufferLength) { var count = _charsUsed - CharPos; if (count > 0) BlockCopyChars(CharBuffer, CharPos, CharBuffer, 0, count); _lineStartPos -= CharPos; CharPos = 0; _charsUsed = count; CharBuffer[_charsUsed] = '\0'; } } private int ReadData(bool append) => ReadData(append, 0); private void PrepareBufferForReadData(bool append, int charsRequired) { MiscellaneousUtils.Assert(CharBuffer != null); // char buffer is full if (_charsUsed + charsRequired >= CharBuffer.Length - 1) { if (append) { var doubledArrayLength = CharBuffer.Length * 2; // copy to new array either double the size of the current or big enough to fit required content var newArrayLength = Math.Max( doubledArrayLength < 0 ? int.MaxValue : doubledArrayLength, // handle overflow _charsUsed + charsRequired + 1); // increase the size of the buffer var dst = BufferUtils.RentBuffer(_arrayPool, newArrayLength); BlockCopyChars(CharBuffer, 0, dst, 0, CharBuffer.Length); BufferUtils.ReturnBuffer(_arrayPool, CharBuffer); CharBuffer = dst; } else { var remainingCharCount = _charsUsed - CharPos; if (remainingCharCount + charsRequired + 1 >= CharBuffer.Length) { // the remaining count plus the required is bigger than the current buffer size var dst = BufferUtils.RentBuffer(_arrayPool, remainingCharCount + charsRequired + 1); if (remainingCharCount > 0) BlockCopyChars(CharBuffer, CharPos, dst, 0, remainingCharCount); BufferUtils.ReturnBuffer(_arrayPool, CharBuffer); CharBuffer = dst; } else { // copy any remaining data to the beginning of the buffer if needed and reset positions if (remainingCharCount > 0) BlockCopyChars(CharBuffer, CharPos, CharBuffer, 0, remainingCharCount); } _lineStartPos -= CharPos; CharPos = 0; _charsUsed = remainingCharCount; } } } private int ReadData(bool append, int charsRequired) { if (_isEndOfFile) return 0; PrepareBufferForReadData(append, charsRequired); MiscellaneousUtils.Assert(CharBuffer != null); var attemptCharReadCount = CharBuffer.Length - _charsUsed - 1; var charsRead = _reader.Read(CharBuffer, _charsUsed, attemptCharReadCount); _charsUsed += charsRead; if (charsRead == 0) _isEndOfFile = true; CharBuffer[_charsUsed] = '\0'; return charsRead; } private bool EnsureChars(int relativePosition, bool append) { if (CharPos + relativePosition >= _charsUsed) return ReadChars(relativePosition, append); return true; } private bool ReadChars(int relativePosition, bool append) { if (_isEndOfFile) return false; var charsRequired = CharPos + relativePosition - _charsUsed + 1; var totalCharsRead = 0; // it is possible that the TextReader doesn't return all data at once // repeat read until the required text is returned or the reader is out of content do { var charsRead = ReadData(append, charsRequired - totalCharsRead); // no more content if (charsRead == 0) break; totalCharsRead += charsRead; } while (totalCharsRead < charsRequired); if (totalCharsRead < charsRequired) return false; return true; } /// <summary> /// Reads the next JSON token from the underlying <see cref="TextReader" />. /// </summary> /// <returns> /// <c>true</c> if the next token was read successfully; <c>false</c> if there are no more tokens to read. /// </returns> public override bool Read() { EnsureBuffer(); MiscellaneousUtils.Assert(CharBuffer != null); while (true) { switch (_currentState) { case State.Start: case State.Property: case State.Array: case State.ArrayStart: case State.Constructor: case State.ConstructorStart: return ParseValue(); case State.Object: case State.ObjectStart: return ParseObject(); case State.PostValue: // returns true if it hits // end of object or array if (ParsePostValue(false)) return true; break; case State.Finished: if (EnsureChars(0, false)) { EatWhitespace(); if (_isEndOfFile) { SetToken(JsonToken.None); return false; } if (CharBuffer[CharPos] == '/') { ParseComment(true); return true; } throw JsonReaderException.Create(this, "Additional text encountered after finished reading JSON content: {0}.".FormatWith(CultureInfo.InvariantCulture, CharBuffer[CharPos])); } SetToken(JsonToken.None); return false; default: throw JsonReaderException.Create(this, "Unexpected state: {0}.".FormatWith(CultureInfo.InvariantCulture, CurrentState)); } } } /// <summary> /// Reads the next JSON token from the underlying <see cref="TextReader" /> as a <see cref="Nullable{T}" /> of /// <see cref="Int32" />. /// </summary> /// <returns> /// A <see cref="Nullable{T}" /> of <see cref="Int32" />. This method will return <c>null</c> at the end of an /// array. /// </returns> public override int? ReadAsInt32() => (int?)ReadNumberValue(ReadType.ReadAsInt32); /// <summary> /// Reads the next JSON token from the underlying <see cref="TextReader" /> as a <see cref="Nullable{T}" /> of /// <see cref="DateTime" />. /// </summary> /// <returns> /// A <see cref="Nullable{T}" /> of <see cref="DateTime" />. This method will return <c>null</c> at the end of an /// array. /// </returns> public override DateTime? ReadAsDateTime() => (DateTime?)ReadStringValue(ReadType.ReadAsDateTime); /// <summary> /// Reads the next JSON token from the underlying <see cref="TextReader" /> as a <see cref="String" />. /// </summary> /// <returns>A <see cref="String" />. This method will return <c>null</c> at the end of an array.</returns> public override string? ReadAsString() => (string?)ReadStringValue(ReadType.ReadAsString); /// <summary> /// Reads the next JSON token from the underlying <see cref="TextReader" /> as a <see cref="Byte" />[]. /// </summary> /// <returns> /// A <see cref="Byte" />[] or <c>null</c> if the next JSON token is null. This method will return <c>null</c> at /// the end of an array. /// </returns> public override byte[]? ReadAsBytes() { EnsureBuffer(); MiscellaneousUtils.Assert(CharBuffer != null); var isWrapped = false; switch (_currentState) { case State.PostValue: if (ParsePostValue(true)) return null; goto case State.Start; case State.Start: case State.Property: case State.Array: case State.ArrayStart: case State.Constructor: case State.ConstructorStart: while (true) { var currentChar = CharBuffer[CharPos]; switch (currentChar) { case '\0': if (ReadNullChar()) { SetToken(JsonToken.None, null, false); return null; } break; case '"': case '\'': ParseString(currentChar, ReadType.ReadAsBytes); var data = (byte[]?)Value; if (isWrapped) { ReaderReadAndAssert(); if (TokenType != JsonToken.EndObject) throw JsonReaderException.Create(this, "Error reading bytes. Unexpected token: {0}.".FormatWith(CultureInfo.InvariantCulture, TokenType)); SetToken(JsonToken.Bytes, data, false); } return data; case '{': CharPos++; SetToken(JsonToken.StartObject); ReadIntoWrappedTypeObject(); isWrapped = true; break; case '[': CharPos++; SetToken(JsonToken.StartArray); return ReadArrayIntoByteArray(); case 'n': HandleNull(); return null; case '/': ParseComment(false); break; case ',': ProcessValueComma(); break; case ']': CharPos++; if (_currentState == State.Array || _currentState == State.ArrayStart || _currentState == State.PostValue) { SetToken(JsonToken.EndArray); return null; } throw CreateUnexpectedCharacterException(currentChar); case StringUtils.CarriageReturn: ProcessCarriageReturn(false); break; case StringUtils.LineFeed: ProcessLineFeed(); break; case ' ': case StringUtils.Tab: // eat CharPos++; break; default: CharPos++; if (!char.IsWhiteSpace(currentChar)) throw CreateUnexpectedCharacterException(currentChar); // eat break; } } case State.Finished: ReadFinished(); return null; default: throw JsonReaderException.Create(this, "Unexpected state: {0}.".FormatWith(CultureInfo.InvariantCulture, CurrentState)); } } private object? ReadStringValue(ReadType readType) { EnsureBuffer(); MiscellaneousUtils.Assert(CharBuffer != null); switch (_currentState) { case State.PostValue: if (ParsePostValue(true)) return null; goto case State.Start; case State.Start: case State.Property: case State.Array: case State.ArrayStart: case State.Constructor: case State.ConstructorStart: while (true) { var currentChar = CharBuffer[CharPos]; switch (currentChar) { case '\0': if (ReadNullChar()) { SetToken(JsonToken.None, null, false); return null; } break; case '"': case '\'': ParseString(currentChar, readType); return FinishReadQuotedStringValue(readType); case '-': if (EnsureChars(1, true) && CharBuffer[CharPos + 1] == 'I') return ParseNumberNegativeInfinity(readType); else { ParseNumber(readType); return Value; } case '.': case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': if (readType != ReadType.ReadAsString) { CharPos++; throw CreateUnexpectedCharacterException(currentChar); } ParseNumber(ReadType.ReadAsString); return Value; case 't': case 'f': if (readType != ReadType.ReadAsString) { CharPos++; throw CreateUnexpectedCharacterException(currentChar); } var expected = currentChar == 't' ? JsonConvert.True : JsonConvert.False; if (!MatchValueWithTrailingSeparator(expected)) throw CreateUnexpectedCharacterException(CharBuffer[CharPos]); SetToken(JsonToken.String, expected); return expected; case 'I': return ParseNumberPositiveInfinity(readType); case 'N': return ParseNumberNaN(readType); case 'n': HandleNull(); return null; case '/': ParseComment(false); break; case ',': ProcessValueComma(); break; case ']': CharPos++; if (_currentState == State.Array || _currentState == State.ArrayStart || _currentState == State.PostValue) { SetToken(JsonToken.EndArray); return null; } throw CreateUnexpectedCharacterException(currentChar); case StringUtils.CarriageReturn: ProcessCarriageReturn(false); break; case StringUtils.LineFeed: ProcessLineFeed(); break; case ' ': case StringUtils.Tab: // eat CharPos++; break; default: CharPos++; if (!char.IsWhiteSpace(currentChar)) throw CreateUnexpectedCharacterException(currentChar); // eat break; } } case State.Finished: ReadFinished(); return null; default: throw JsonReaderException.Create(this, "Unexpected state: {0}.".FormatWith(CultureInfo.InvariantCulture, CurrentState)); } } private object? FinishReadQuotedStringValue(ReadType readType) { switch (readType) { case ReadType.ReadAsBytes: case ReadType.ReadAsString: return Value; case ReadType.ReadAsDateTime: if (Value is DateTime time) return time; return ReadDateTimeString((string?)Value); #if HAVE_DATE_TIME_OFFSET case ReadType.ReadAsDateTimeOffset: if (Value is DateTimeOffset offset) { return offset; } return ReadDateTimeOffsetString((string?)Value); #endif default: throw new ArgumentOutOfRangeException(nameof(readType)); } } private JsonReaderException CreateUnexpectedCharacterException(char c) => JsonReaderException.Create(this, "Unexpected character encountered while parsing value: {0}.".FormatWith(CultureInfo.InvariantCulture, c)); /// <summary> /// Reads the next JSON token from the underlying <see cref="TextReader" /> as a <see cref="Nullable{T}" /> of /// <see cref="Boolean" />. /// </summary> /// <returns> /// A <see cref="Nullable{T}" /> of <see cref="Boolean" />. This method will return <c>null</c> at the end of an /// array. /// </returns> public override bool? ReadAsBoolean() { EnsureBuffer(); MiscellaneousUtils.Assert(CharBuffer != null); switch (_currentState) { case State.PostValue: if (ParsePostValue(true)) return null; goto case State.Start; case State.Start: case State.Property: case State.Array: case State.ArrayStart: case State.Constructor: case State.ConstructorStart: while (true) { var currentChar = CharBuffer[CharPos]; switch (currentChar) { case '\0': if (ReadNullChar()) { SetToken(JsonToken.None, null, false); return null; } break; case '"': case '\'': ParseString(currentChar, ReadType.Read); return ReadBooleanString(_stringReference.ToString()); case 'n': HandleNull(); return null; case '-': case '.': case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': ParseNumber(ReadType.Read); bool b; #if HAVE_BIG_INTEGER if (Value is BigInteger integer) { b = integer != 0; } else #endif { b = Convert.ToBoolean(Value, CultureInfo.InvariantCulture); } SetToken(JsonToken.Boolean, b, false); return b; case 't': case 'f': var isTrue = currentChar == 't'; var expected = isTrue ? JsonConvert.True : JsonConvert.False; if (!MatchValueWithTrailingSeparator(expected)) throw CreateUnexpectedCharacterException(CharBuffer[CharPos]); SetToken(JsonToken.Boolean, isTrue); return isTrue; case '/': ParseComment(false); break; case ',': ProcessValueComma(); break; case ']': CharPos++; if (_currentState == State.Array || _currentState == State.ArrayStart || _currentState == State.PostValue) { SetToken(JsonToken.EndArray); return null; } throw CreateUnexpectedCharacterException(currentChar); case StringUtils.CarriageReturn: ProcessCarriageReturn(false); break; case StringUtils.LineFeed: ProcessLineFeed(); break; case ' ': case StringUtils.Tab: // eat CharPos++; break; default: CharPos++; if (!char.IsWhiteSpace(currentChar)) throw CreateUnexpectedCharacterException(currentChar); // eat break; } } case State.Finished: ReadFinished(); return null; default: throw JsonReaderException.Create(this, "Unexpected state: {0}.".FormatWith(CultureInfo.InvariantCulture, CurrentState)); } } private void ProcessValueComma() { CharPos++; if (_currentState != State.PostValue) { SetToken(JsonToken.Undefined); var ex = CreateUnexpectedCharacterException(','); // so the comma will be parsed again CharPos--; throw ex; } SetStateBasedOnCurrent(); } private object? ReadNumberValue(ReadType readType) { EnsureBuffer(); MiscellaneousUtils.Assert(CharBuffer != null); switch (_currentState) { case State.PostValue: if (ParsePostValue(true)) return null; goto case State.Start; case State.Start: case State.Property: case State.Array: case State.ArrayStart: case State.Constructor: case State.ConstructorStart: while (true) { var currentChar = CharBuffer[CharPos]; switch (currentChar) { case '\0': if (ReadNullChar()) { SetToken(JsonToken.None, null, false); return null; } break; case '"': case '\'': ParseString(currentChar, readType); return FinishReadQuotedNumber(readType); case 'n': HandleNull(); return null; case 'N': return ParseNumberNaN(readType); case 'I': return ParseNumberPositiveInfinity(readType); case '-': if (EnsureChars(1, true) && CharBuffer[CharPos + 1] == 'I') return ParseNumberNegativeInfinity(readType); else { ParseNumber(readType); return Value; } case '.': case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': ParseNumber(readType); return Value; case '/': ParseComment(false); break; case ',': ProcessValueComma(); break; case ']': CharPos++; if (_currentState == State.Array || _currentState == State.ArrayStart || _currentState == State.PostValue) { SetToken(JsonToken.EndArray); return null; } throw CreateUnexpectedCharacterException(currentChar); case StringUtils.CarriageReturn: ProcessCarriageReturn(false); break; case StringUtils.LineFeed: ProcessLineFeed(); break; case ' ': case StringUtils.Tab: // eat CharPos++; break; default: CharPos++; if (!char.IsWhiteSpace(currentChar)) throw CreateUnexpectedCharacterException(currentChar); // eat break; } } case State.Finished: ReadFinished(); return null; default: throw JsonReaderException.Create(this, "Unexpected state: {0}.".FormatWith(CultureInfo.InvariantCulture, CurrentState)); } } private object? FinishReadQuotedNumber(ReadType readType) { switch (readType) { case ReadType.ReadAsInt32: return ReadInt32String(_stringReference.ToString()); case ReadType.ReadAsDecimal: return ReadDecimalString(_stringReference.ToString()); case ReadType.ReadAsDouble: return ReadDoubleString(_stringReference.ToString()); default: throw new ArgumentOutOfRangeException(nameof(readType)); } } #if HAVE_DATE_TIME_OFFSET /// <summary> /// Reads the next JSON token from the underlying <see cref="TextReader"/> as a <see cref="Nullable{T}"/> of <see cref="DateTimeOffset"/>. /// </summary> /// <returns>A <see cref="Nullable{T}"/> of <see cref="DateTimeOffset"/>. This method will return <c>null</c> at the end of an array.</returns> public override DateTimeOffset? ReadAsDateTimeOffset() { return (DateTimeOffset?)ReadStringValue(ReadType.ReadAsDateTimeOffset); } #endif /// <summary> /// Reads the next JSON token from the underlying <see cref="TextReader" /> as a <see cref="Nullable{T}" /> of /// <see cref="Decimal" />. /// </summary> /// <returns> /// A <see cref="Nullable{T}" /> of <see cref="Decimal" />. This method will return <c>null</c> at the end of an /// array. /// </returns> public override decimal? ReadAsDecimal() => (decimal?)ReadNumberValue(ReadType.ReadAsDecimal); /// <summary> /// Reads the next JSON token from the underlying <see cref="TextReader" /> as a <see cref="Nullable{T}" /> of /// <see cref="Double" />. /// </summary> /// <returns> /// A <see cref="Nullable{T}" /> of <see cref="Double" />. This method will return <c>null</c> at the end of an /// array. /// </returns> public override double? ReadAsDouble() => (double?)ReadNumberValue(ReadType.ReadAsDouble); private void HandleNull() { MiscellaneousUtils.Assert(CharBuffer != null); if (EnsureChars(1, true)) { var next = CharBuffer[CharPos + 1]; if (next == 'u') { ParseNull(); return; } CharPos += 2; throw CreateUnexpectedCharacterException(CharBuffer[CharPos - 1]); } CharPos = _charsUsed; throw CreateUnexpectedEndException(); } private void ReadFinished() { MiscellaneousUtils.Assert(CharBuffer != null); if (EnsureChars(0, false)) { EatWhitespace(); if (_isEndOfFile) return; if (CharBuffer[CharPos] == '/') ParseComment(false); else throw JsonReaderException.Create(this, "Additional text encountered after finished reading JSON content: {0}.".FormatWith(CultureInfo.InvariantCulture, CharBuffer[CharPos])); } SetToken(JsonToken.None); } private bool ReadNullChar() { if (_charsUsed == CharPos) { if (ReadData(false) == 0) { _isEndOfFile = true; return true; } } else CharPos++; return false; } private void EnsureBuffer() { if (CharBuffer == null) { CharBuffer = BufferUtils.RentBuffer(_arrayPool, 1024); CharBuffer[0] = '\0'; } } private void ReadStringIntoBuffer(char quote) { MiscellaneousUtils.Assert(CharBuffer != null); var charPos = CharPos; var initialPosition = CharPos; var lastWritePosition = CharPos; _stringBuffer.Position = 0; while (true) { switch (CharBuffer[charPos++]) { case '\0': if (_charsUsed == charPos - 1) { charPos--; if (ReadData(true) == 0) { CharPos = charPos; throw JsonReaderException.Create(this, "Unterminated string. Expected delimiter: {0}.".FormatWith(CultureInfo.InvariantCulture, quote)); } } break; case '\\': CharPos = charPos; if (!EnsureChars(0, true)) throw JsonReaderException.Create(this, "Unterminated string. Expected delimiter: {0}.".FormatWith(CultureInfo.InvariantCulture, quote)); // start of escape sequence var escapeStartPos = charPos - 1; var currentChar = CharBuffer[charPos]; charPos++; char writeChar; switch (currentChar) { case 'b': writeChar = '\b'; break; case 't': writeChar = '\t'; break; case 'n': writeChar = '\n'; break; case 'f': writeChar = '\f'; break; case 'r': writeChar = '\r'; break; case '\\': writeChar = '\\'; break; case '"': case '\'': case '/': writeChar = currentChar; break; case 'u': CharPos = charPos; writeChar = ParseUnicode(); if (StringUtils.IsLowSurrogate(writeChar)) { // low surrogate with no preceding high surrogate; this char is replaced writeChar = UnicodeReplacementChar; } else if (StringUtils.IsHighSurrogate(writeChar)) { bool anotherHighSurrogate; // loop for handling situations where there are multiple consecutive high surrogates do { anotherHighSurrogate = false; // potential start of a surrogate pair if (EnsureChars(2, true) && CharBuffer[CharPos] == '\\' && CharBuffer[CharPos + 1] == 'u') { var highSurrogate = writeChar; CharPos += 2; writeChar = ParseUnicode(); if (StringUtils.IsLowSurrogate(writeChar)) { // a valid surrogate pair! } else if (StringUtils.IsHighSurrogate(writeChar)) { // another high surrogate; replace current and start check over highSurrogate = UnicodeReplacementChar; anotherHighSurrogate = true; } else { // high surrogate not followed by low surrogate; original char is replaced highSurrogate = UnicodeReplacementChar; } EnsureBufferNotEmpty(); WriteCharToBuffer(highSurrogate, lastWritePosition, escapeStartPos); lastWritePosition = CharPos; } else { // there are not enough remaining chars for the low surrogate or is not follow by unicode sequence // replace high surrogate and continue on as usual writeChar = UnicodeReplacementChar; } } while (anotherHighSurrogate); } charPos = CharPos; break; default: CharPos = charPos; throw JsonReaderException.Create(this, "Bad JSON escape sequence: {0}.".FormatWith(CultureInfo.InvariantCulture, @"\" + currentChar)); } EnsureBufferNotEmpty(); WriteCharToBuffer(writeChar, lastWritePosition, escapeStartPos); lastWritePosition = charPos; break; case StringUtils.CarriageReturn: CharPos = charPos - 1; ProcessCarriageReturn(true); charPos = CharPos; break; case StringUtils.LineFeed: CharPos = charPos - 1; ProcessLineFeed(); charPos = CharPos; break; case '"': case '\'': if (CharBuffer[charPos - 1] == quote) { FinishReadStringIntoBuffer(charPos - 1, initialPosition, lastWritePosition); return; } break; } } } private void FinishReadStringIntoBuffer(int charPos, int initialPosition, int lastWritePosition) { MiscellaneousUtils.Assert(CharBuffer != null); if (initialPosition == lastWritePosition) _stringReference = new StringReference(CharBuffer, initialPosition, charPos - initialPosition); else { EnsureBufferNotEmpty(); if (charPos > lastWritePosition) _stringBuffer.Append(_arrayPool, CharBuffer, lastWritePosition, charPos - lastWritePosition); _stringReference = new StringReference(_stringBuffer.InternalBuffer!, 0, _stringBuffer.Position); } CharPos = charPos + 1; } private void WriteCharToBuffer(char writeChar, int lastWritePosition, int writeToPosition) { MiscellaneousUtils.Assert(CharBuffer != null); if (writeToPosition > lastWritePosition) _stringBuffer.Append(_arrayPool, CharBuffer, lastWritePosition, writeToPosition - lastWritePosition); _stringBuffer.Append(_arrayPool, writeChar); } private char ConvertUnicode(bool enoughChars) { MiscellaneousUtils.Assert(CharBuffer != null); if (enoughChars) { if (ConvertUtils.TryHexTextToInt(CharBuffer, CharPos, CharPos + 4, out var value)) { var hexChar = Convert.ToChar(value); CharPos += 4; return hexChar; } throw JsonReaderException.Create(this, @"Invalid Unicode escape sequence: \u{0}.".FormatWith(CultureInfo.InvariantCulture, new string(CharBuffer, CharPos, 4))); } throw JsonReaderException.Create(this, "Unexpected end while parsing Unicode escape sequence."); } private char ParseUnicode() => ConvertUnicode(EnsureChars(4, true)); private void ReadNumberIntoBuffer() { MiscellaneousUtils.Assert(CharBuffer != null); var charPos = CharPos; while (true) { var currentChar = CharBuffer[charPos]; if (currentChar == '\0') { CharPos = charPos; if (_charsUsed == charPos) { if (ReadData(true) == 0) return; } else return; } else if (ReadNumberCharIntoBuffer(currentChar, charPos)) return; else charPos++; } } private bool ReadNumberCharIntoBuffer(char currentChar, int charPos) { switch (currentChar) { case '-': case '+': case 'a': case 'A': case 'b': case 'B': case 'c': case 'C': case 'd': case 'D': case 'e': case 'E': case 'f': case 'F': case 'x': case 'X': case '.': case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': return false; default: CharPos = charPos; if (char.IsWhiteSpace(currentChar) || currentChar == ',' || currentChar == '}' || currentChar == ']' || currentChar == ')' || currentChar == '/') return true; throw JsonReaderException.Create(this, "Unexpected character encountered while parsing number: {0}.".FormatWith(CultureInfo.InvariantCulture, currentChar)); } } private void ClearRecentString() { _stringBuffer.Position = 0; _stringReference = new StringReference(); } private bool ParsePostValue(bool ignoreComments) { MiscellaneousUtils.Assert(CharBuffer != null); while (true) { var currentChar = CharBuffer[CharPos]; switch (currentChar) { case '\0': if (_charsUsed == CharPos) { if (ReadData(false) == 0) { _currentState = State.Finished; return false; } } else CharPos++; break; case '}': CharPos++; SetToken(JsonToken.EndObject); return true; case ']': CharPos++; SetToken(JsonToken.EndArray); return true; case ')': CharPos++; SetToken(JsonToken.EndConstructor); return true; case '/': ParseComment(!ignoreComments); if (!ignoreComments) return true; break; case ',': CharPos++; // finished parsing SetStateBasedOnCurrent(); return false; case ' ': case StringUtils.Tab: // eat CharPos++; break; case StringUtils.CarriageReturn: ProcessCarriageReturn(false); break; case StringUtils.LineFeed: ProcessLineFeed(); break; default: if (char.IsWhiteSpace(currentChar)) { // eat CharPos++; } else { // handle multiple content without comma delimiter if (SupportMultipleContent && Depth == 0) { SetStateBasedOnCurrent(); return false; } throw JsonReaderException.Create(this, "After parsing a value an unexpected character was encountered: {0}.".FormatWith(CultureInfo.InvariantCulture, currentChar)); } break; } } } private bool ParseObject() { MiscellaneousUtils.Assert(CharBuffer != null); while (true) { var currentChar = CharBuffer[CharPos]; switch (currentChar) { case '\0': if (_charsUsed == CharPos) { if (ReadData(false) == 0) return false; } else CharPos++; break; case '}': SetToken(JsonToken.EndObject); CharPos++; return true; case '/': ParseComment(true); return true; case StringUtils.CarriageReturn: ProcessCarriageReturn(false); break; case StringUtils.LineFeed: ProcessLineFeed(); break; case ' ': case StringUtils.Tab: // eat CharPos++; break; default: if (char.IsWhiteSpace(currentChar)) { // eat CharPos++; } else return ParseProperty(); break; } } } private bool ParseProperty() { MiscellaneousUtils.Assert(CharBuffer != null); var firstChar = CharBuffer[CharPos]; char quoteChar; if (firstChar == '"' || firstChar == '\'') { CharPos++; quoteChar = firstChar; ShiftBufferIfNeeded(); ReadStringIntoBuffer(quoteChar); } else if (ValidIdentifierChar(firstChar)) { quoteChar = '\0'; ShiftBufferIfNeeded(); ParseUnquotedProperty(); } else throw JsonReaderException.Create(this, "Invalid property identifier character: {0}.".FormatWith(CultureInfo.InvariantCulture, CharBuffer[CharPos])); string? propertyName; if (PropertyNameTable != null) { propertyName = PropertyNameTable.Get(_stringReference.Chars, _stringReference.StartIndex, _stringReference.Length); // no match in name table if (propertyName == null) propertyName = _stringReference.ToString(); } else propertyName = _stringReference.ToString(); EatWhitespace(); if (CharBuffer[CharPos] != ':') throw JsonReaderException.Create(this, "Invalid character after parsing property name. Expected ':' but got: {0}.".FormatWith(CultureInfo.InvariantCulture, CharBuffer[CharPos])); CharPos++; SetToken(JsonToken.PropertyName, propertyName); _quoteChar = quoteChar; ClearRecentString(); return true; } private bool ValidIdentifierChar(char value) => char.IsLetterOrDigit(value) || value == '_' || value == '$'; private void ParseUnquotedProperty() { MiscellaneousUtils.Assert(CharBuffer != null); var initialPosition = CharPos; // parse unquoted property name until whitespace or colon while (true) { var currentChar = CharBuffer[CharPos]; if (currentChar == '\0') { if (_charsUsed == CharPos) { if (ReadData(true) == 0) throw JsonReaderException.Create(this, "Unexpected end while parsing unquoted property name."); continue; } _stringReference = new StringReference(CharBuffer, initialPosition, CharPos - initialPosition); return; } if (ReadUnquotedPropertyReportIfDone(currentChar, initialPosition)) return; } } private bool ReadUnquotedPropertyReportIfDone(char currentChar, int initialPosition) { MiscellaneousUtils.Assert(CharBuffer != null); if (ValidIdentifierChar(currentChar)) { CharPos++; return false; } if (char.IsWhiteSpace(currentChar) || currentChar == ':') { _stringReference = new StringReference(CharBuffer, initialPosition, CharPos - initialPosition); return true; } throw JsonReaderException.Create(this, "Invalid JavaScript property identifier character: {0}.".FormatWith(CultureInfo.InvariantCulture, currentChar)); } private bool ParseValue() { MiscellaneousUtils.Assert(CharBuffer != null); while (true) { var currentChar = CharBuffer[CharPos]; switch (currentChar) { case '\0': if (_charsUsed == CharPos) { if (ReadData(false) == 0) return false; } else CharPos++; break; case '"': case '\'': ParseString(currentChar, ReadType.Read); return true; case 't': ParseTrue(); return true; case 'f': ParseFalse(); return true; case 'n': if (EnsureChars(1, true)) { var next = CharBuffer[CharPos + 1]; if (next == 'u') ParseNull(); else if (next == 'e') ParseConstructor(); else throw CreateUnexpectedCharacterException(CharBuffer[CharPos]); } else { CharPos++; throw CreateUnexpectedEndException(); } return true; case 'N': ParseNumberNaN(ReadType.Read); return true; case 'I': ParseNumberPositiveInfinity(ReadType.Read); return true; case '-': if (EnsureChars(1, true) && CharBuffer[CharPos + 1] == 'I') ParseNumberNegativeInfinity(ReadType.Read); else ParseNumber(ReadType.Read); return true; case '/': ParseComment(true); return true; case 'u': ParseUndefined(); return true; case '{': CharPos++; SetToken(JsonToken.StartObject); return true; case '[': CharPos++; SetToken(JsonToken.StartArray); return true; case ']': CharPos++; SetToken(JsonToken.EndArray); return true; case ',': // don't increment position, the next call to read will handle comma // this is done to handle multiple empty comma values SetToken(JsonToken.Undefined); return true; case ')': CharPos++; SetToken(JsonToken.EndConstructor); return true; case StringUtils.CarriageReturn: ProcessCarriageReturn(false); break; case StringUtils.LineFeed: ProcessLineFeed(); break; case ' ': case StringUtils.Tab: // eat CharPos++; break; default: if (char.IsWhiteSpace(currentChar)) { // eat CharPos++; break; } if (char.IsNumber(currentChar) || currentChar == '-' || currentChar == '.') { ParseNumber(ReadType.Read); return true; } throw CreateUnexpectedCharacterException(currentChar); } } } private void ProcessLineFeed() { CharPos++; OnNewLine(CharPos); } private void ProcessCarriageReturn(bool append) { CharPos++; SetNewLine(EnsureChars(1, append)); } private void EatWhitespace() { MiscellaneousUtils.Assert(CharBuffer != null); while (true) { var currentChar = CharBuffer[CharPos]; switch (currentChar) { case '\0': if (_charsUsed == CharPos) { if (ReadData(false) == 0) return; } else CharPos++; break; case StringUtils.CarriageReturn: ProcessCarriageReturn(false); break; case StringUtils.LineFeed: ProcessLineFeed(); break; default: if (currentChar == ' ' || char.IsWhiteSpace(currentChar)) CharPos++; else return; break; } } } private void ParseConstructor() { MiscellaneousUtils.Assert(CharBuffer != null); if (MatchValueWithTrailingSeparator("new")) { EatWhitespace(); var initialPosition = CharPos; int endPosition; while (true) { var currentChar = CharBuffer[CharPos]; if (currentChar == '\0') { if (_charsUsed == CharPos) { if (ReadData(true) == 0) throw JsonReaderException.Create(this, "Unexpected end while parsing constructor."); } else { endPosition = CharPos; CharPos++; break; } } else if (char.IsLetterOrDigit(currentChar)) CharPos++; else if (currentChar == StringUtils.CarriageReturn) { endPosition = CharPos; ProcessCarriageReturn(true); break; } else if (currentChar == StringUtils.LineFeed) { endPosition = CharPos; ProcessLineFeed(); break; } else if (char.IsWhiteSpace(currentChar)) { endPosition = CharPos; CharPos++; break; } else if (currentChar == '(') { endPosition = CharPos; break; } else throw JsonReaderException.Create(this, "Unexpected character while parsing constructor: {0}.".FormatWith(CultureInfo.InvariantCulture, currentChar)); } _stringReference = new StringReference(CharBuffer, initialPosition, endPosition - initialPosition); var constructorName = _stringReference.ToString(); EatWhitespace(); if (CharBuffer[CharPos] != '(') throw JsonReaderException.Create(this, "Unexpected character while parsing constructor: {0}.".FormatWith(CultureInfo.InvariantCulture, CharBuffer[CharPos])); CharPos++; ClearRecentString(); SetToken(JsonToken.StartConstructor, constructorName); } else throw JsonReaderException.Create(this, "Unexpected content while parsing JSON."); } private void ParseNumber(ReadType readType) { ShiftBufferIfNeeded(); MiscellaneousUtils.Assert(CharBuffer != null); var firstChar = CharBuffer[CharPos]; var initialPosition = CharPos; ReadNumberIntoBuffer(); ParseReadNumber(readType, firstChar, initialPosition); } private void ParseReadNumber(ReadType readType, char firstChar, int initialPosition) { MiscellaneousUtils.Assert(CharBuffer != null); // set state to PostValue now so that if there is an error parsing the number then the reader can continue SetPostValueState(true); _stringReference = new StringReference(CharBuffer, initialPosition, CharPos - initialPosition); object numberValue; JsonToken numberType; var singleDigit = char.IsDigit(firstChar) && _stringReference.Length == 1; var nonBase10 = firstChar == '0' && _stringReference.Length > 1 && _stringReference.Chars[_stringReference.StartIndex + 1] != '.' && _stringReference.Chars[_stringReference.StartIndex + 1] != 'e' && _stringReference.Chars[_stringReference.StartIndex + 1] != 'E'; switch (readType) { case ReadType.ReadAsString: { var number = _stringReference.ToString(); // validate that the string is a valid number if (nonBase10) { try { if (number.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) Convert.ToInt64(number, 16); else Convert.ToInt64(number, 8); } catch (Exception ex) { throw ThrowReaderError("Input string '{0}' is not a valid number.".FormatWith(CultureInfo.InvariantCulture, number), ex); } } else { if (!double.TryParse(number, NumberStyles.Float, CultureInfo.InvariantCulture, out _)) throw ThrowReaderError("Input string '{0}' is not a valid number.".FormatWith(CultureInfo.InvariantCulture, _stringReference.ToString())); } numberType = JsonToken.String; numberValue = number; } break; case ReadType.ReadAsInt32: { if (singleDigit) { // digit char values start at 48 numberValue = firstChar - 48; } else if (nonBase10) { var number = _stringReference.ToString(); try { var integer = number.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? Convert.ToInt32(number, 16) : Convert.ToInt32(number, 8); numberValue = integer; } catch (Exception ex) { throw ThrowReaderError("Input string '{0}' is not a valid integer.".FormatWith(CultureInfo.InvariantCulture, number), ex); } } else { var parseResult = ConvertUtils.Int32TryParse(_stringReference.Chars, _stringReference.StartIndex, _stringReference.Length, out var value); if (parseResult == ParseResult.Success) numberValue = value; else if (parseResult == ParseResult.Overflow) throw ThrowReaderError( "JSON integer {0} is too large or small for an Int32.".FormatWith(CultureInfo.InvariantCulture, _stringReference.ToString())); else throw ThrowReaderError("Input string '{0}' is not a valid integer.".FormatWith(CultureInfo.InvariantCulture, _stringReference.ToString())); } numberType = JsonToken.Integer; } break; case ReadType.ReadAsDecimal: { if (singleDigit) { // digit char values start at 48 numberValue = (decimal)firstChar - 48; } else if (nonBase10) { var number = _stringReference.ToString(); try { // decimal.Parse doesn't support parsing hexadecimal values var integer = number.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? Convert.ToInt64(number, 16) : Convert.ToInt64(number, 8); numberValue = Convert.ToDecimal(integer); } catch (Exception ex) { throw ThrowReaderError("Input string '{0}' is not a valid decimal.".FormatWith(CultureInfo.InvariantCulture, number), ex); } } else { var parseResult = ConvertUtils.DecimalTryParse(_stringReference.Chars, _stringReference.StartIndex, _stringReference.Length, out var value); if (parseResult == ParseResult.Success) numberValue = value; else throw ThrowReaderError("Input string '{0}' is not a valid decimal.".FormatWith(CultureInfo.InvariantCulture, _stringReference.ToString())); } numberType = JsonToken.Float; } break; case ReadType.ReadAsDouble: { if (singleDigit) { // digit char values start at 48 numberValue = (double)firstChar - 48; } else if (nonBase10) { var number = _stringReference.ToString(); try { // double.Parse doesn't support parsing hexadecimal values var integer = number.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? Convert.ToInt64(number, 16) : Convert.ToInt64(number, 8); numberValue = Convert.ToDouble(integer); } catch (Exception ex) { throw ThrowReaderError("Input string '{0}' is not a valid double.".FormatWith(CultureInfo.InvariantCulture, number), ex); } } else { var number = _stringReference.ToString(); if (double.TryParse(number, NumberStyles.Float, CultureInfo.InvariantCulture, out var value)) numberValue = value; else throw ThrowReaderError("Input string '{0}' is not a valid double.".FormatWith(CultureInfo.InvariantCulture, _stringReference.ToString())); } numberType = JsonToken.Float; } break; case ReadType.Read: case ReadType.ReadAsInt64: { if (singleDigit) { // digit char values start at 48 numberValue = (long)firstChar - 48; numberType = JsonToken.Integer; } else if (nonBase10) { var number = _stringReference.ToString(); try { numberValue = number.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? Convert.ToInt64(number, 16) : Convert.ToInt64(number, 8); } catch (Exception ex) { throw ThrowReaderError("Input string '{0}' is not a valid number.".FormatWith(CultureInfo.InvariantCulture, number), ex); } numberType = JsonToken.Integer; } else { var parseResult = ConvertUtils.Int64TryParse(_stringReference.Chars, _stringReference.StartIndex, _stringReference.Length, out var value); if (parseResult == ParseResult.Success) { numberValue = value; numberType = JsonToken.Integer; } else if (parseResult == ParseResult.Overflow) { #if HAVE_BIG_INTEGER string number = _stringReference.ToString(); if (number.Length > MaximumJavascriptIntegerCharacterLength) { throw ThrowReaderError("JSON integer {0} is too large to parse.".FormatWith(CultureInfo.InvariantCulture, _stringReference.ToString())); } numberValue = BigIntegerParse(number, CultureInfo.InvariantCulture); numberType = JsonToken.Integer; #else throw ThrowReaderError( "JSON integer {0} is too large or small for an Int64.".FormatWith(CultureInfo.InvariantCulture, _stringReference.ToString())); #endif } else { if (_floatParseHandling == FloatParseHandling.Decimal) { parseResult = ConvertUtils.DecimalTryParse(_stringReference.Chars, _stringReference.StartIndex, _stringReference.Length, out var d); if (parseResult == ParseResult.Success) numberValue = d; else throw ThrowReaderError("Input string '{0}' is not a valid decimal.".FormatWith(CultureInfo.InvariantCulture, _stringReference.ToString())); } else { var number = _stringReference.ToString(); if (double.TryParse(number, NumberStyles.Float, CultureInfo.InvariantCulture, out var d)) numberValue = d; else throw ThrowReaderError("Input string '{0}' is not a valid number.".FormatWith(CultureInfo.InvariantCulture, _stringReference.ToString())); } numberType = JsonToken.Float; } } } break; default: throw JsonReaderException.Create(this, "Cannot read number value as type."); } ClearRecentString(); // index has already been updated SetToken(numberType, numberValue, false); } private JsonReaderException ThrowReaderError(string message, Exception? ex = null) { SetToken(JsonToken.Undefined, null, false); return JsonReaderException.Create(this, message, ex); } #if HAVE_BIG_INTEGER // By using the BigInteger type in a separate method, // the runtime can execute the ParseNumber even if // the System.Numerics.BigInteger.Parse method is // missing, which happens in some versions of Mono [MethodImpl(MethodImplOptions.NoInlining)] private static object BigIntegerParse(string number, CultureInfo culture) { return System.Numerics.BigInteger.Parse(number, culture); } #endif private void ParseComment(bool setToken) { MiscellaneousUtils.Assert(CharBuffer != null); // should have already parsed / character before reaching this method CharPos++; if (!EnsureChars(1, false)) throw JsonReaderException.Create(this, "Unexpected end while parsing comment."); bool singlelineComment; if (CharBuffer[CharPos] == '*') singlelineComment = false; else if (CharBuffer[CharPos] == '/') singlelineComment = true; else throw JsonReaderException.Create(this, "Error parsing comment. Expected: *, got {0}.".FormatWith(CultureInfo.InvariantCulture, CharBuffer[CharPos])); CharPos++; var initialPosition = CharPos; while (true) { switch (CharBuffer[CharPos]) { case '\0': if (_charsUsed == CharPos) { if (ReadData(true) == 0) { if (!singlelineComment) throw JsonReaderException.Create(this, "Unexpected end while parsing comment."); EndComment(setToken, initialPosition, CharPos); return; } } else CharPos++; break; case '*': CharPos++; if (!singlelineComment) { if (EnsureChars(0, true)) { if (CharBuffer[CharPos] == '/') { EndComment(setToken, initialPosition, CharPos - 1); CharPos++; return; } } } break; case StringUtils.CarriageReturn: if (singlelineComment) { EndComment(setToken, initialPosition, CharPos); return; } ProcessCarriageReturn(true); break; case StringUtils.LineFeed: if (singlelineComment) { EndComment(setToken, initialPosition, CharPos); return; } ProcessLineFeed(); break; default: CharPos++; break; } } } private void EndComment(bool setToken, int initialPosition, int endPosition) { if (setToken) SetToken(JsonToken.Comment, new string(CharBuffer, initialPosition, endPosition - initialPosition)); } private bool MatchValue(string value) => MatchValue(EnsureChars(value.Length - 1, true), value); private bool MatchValue(bool enoughChars, string value) { MiscellaneousUtils.Assert(CharBuffer != null); if (!enoughChars) { CharPos = _charsUsed; throw CreateUnexpectedEndException(); } for (var i = 0; i < value.Length; i++) { if (CharBuffer[CharPos + i] != value[i]) { CharPos += i; return false; } } CharPos += value.Length; return true; } private bool MatchValueWithTrailingSeparator(string value) { MiscellaneousUtils.Assert(CharBuffer != null); // will match value and then move to the next character, checking that it is a separator character var match = MatchValue(value); if (!match) return false; if (!EnsureChars(0, false)) return true; return IsSeparator(CharBuffer[CharPos]) || CharBuffer[CharPos] == '\0'; } private bool IsSeparator(char c) { MiscellaneousUtils.Assert(CharBuffer != null); switch (c) { case '}': case ']': case ',': return true; case '/': // check next character to see if start of a comment if (!EnsureChars(1, false)) return false; var nextChart = CharBuffer[CharPos + 1]; return nextChart == '*' || nextChart == '/'; case ')': if (CurrentState == State.Constructor || CurrentState == State.ConstructorStart) return true; break; case ' ': case StringUtils.Tab: case StringUtils.LineFeed: case StringUtils.CarriageReturn: return true; default: if (char.IsWhiteSpace(c)) return true; break; } return false; } private void ParseTrue() { // check characters equal 'true' // and that it is followed by either a separator character // or the text ends if (MatchValueWithTrailingSeparator(JsonConvert.True)) SetToken(JsonToken.Boolean, true); else throw JsonReaderException.Create(this, "Error parsing boolean value."); } private void ParseNull() { if (MatchValueWithTrailingSeparator(JsonConvert.Null)) SetToken(JsonToken.Null); else throw JsonReaderException.Create(this, "Error parsing null value."); } private void ParseUndefined() { if (MatchValueWithTrailingSeparator(JsonConvert.Undefined)) SetToken(JsonToken.Undefined); else throw JsonReaderException.Create(this, "Error parsing undefined value."); } private void ParseFalse() { if (MatchValueWithTrailingSeparator(JsonConvert.False)) SetToken(JsonToken.Boolean, false); else throw JsonReaderException.Create(this, "Error parsing boolean value."); } private object ParseNumberNegativeInfinity(ReadType readType) => ParseNumberNegativeInfinity(readType, MatchValueWithTrailingSeparator(JsonConvert.NegativeInfinity)); private object ParseNumberNegativeInfinity(ReadType readType, bool matched) { if (matched) { switch (readType) { case ReadType.Read: case ReadType.ReadAsDouble: if (_floatParseHandling == FloatParseHandling.Double) { SetToken(JsonToken.Float, double.NegativeInfinity); return double.NegativeInfinity; } break; case ReadType.ReadAsString: SetToken(JsonToken.String, JsonConvert.NegativeInfinity); return JsonConvert.NegativeInfinity; } throw JsonReaderException.Create(this, "Cannot read -Infinity value."); } throw JsonReaderException.Create(this, "Error parsing -Infinity value."); } private object ParseNumberPositiveInfinity(ReadType readType) => ParseNumberPositiveInfinity(readType, MatchValueWithTrailingSeparator(JsonConvert.PositiveInfinity)); private object ParseNumberPositiveInfinity(ReadType readType, bool matched) { if (matched) { switch (readType) { case ReadType.Read: case ReadType.ReadAsDouble: if (_floatParseHandling == FloatParseHandling.Double) { SetToken(JsonToken.Float, double.PositiveInfinity); return double.PositiveInfinity; } break; case ReadType.ReadAsString: SetToken(JsonToken.String, JsonConvert.PositiveInfinity); return JsonConvert.PositiveInfinity; } throw JsonReaderException.Create(this, "Cannot read Infinity value."); } throw JsonReaderException.Create(this, "Error parsing Infinity value."); } private object ParseNumberNaN(ReadType readType) => ParseNumberNaN(readType, MatchValueWithTrailingSeparator(JsonConvert.NaN)); private object ParseNumberNaN(ReadType readType, bool matched) { if (matched) { switch (readType) { case ReadType.Read: case ReadType.ReadAsDouble: if (_floatParseHandling == FloatParseHandling.Double) { SetToken(JsonToken.Float, double.NaN); return double.NaN; } break; case ReadType.ReadAsString: SetToken(JsonToken.String, JsonConvert.NaN); return JsonConvert.NaN; } throw JsonReaderException.Create(this, "Cannot read NaN value."); } throw JsonReaderException.Create(this, "Error parsing NaN value."); } /// <summary> /// Changes the reader's state to <see cref="JsonReader.State.Closed" />. /// If <see cref="JsonReader.CloseInput" /> is set to <c>true</c>, the underlying <see cref="TextReader" /> is also closed. /// </summary> public override void Close() { base.Close(); if (CharBuffer != null) { BufferUtils.ReturnBuffer(_arrayPool, CharBuffer); CharBuffer = null; } if (CloseInput) { #if HAVE_STREAM_READER_WRITER_CLOSE _reader?.Close(); #else _reader?.Dispose(); #endif } _stringBuffer.Clear(_arrayPool); } /// <summary> /// Gets a value indicating whether the class can return line information. /// </summary> /// <returns> /// <c>true</c> if <see cref="JsonTextReader.LineNumber" /> and <see cref="JsonTextReader.LinePosition" /> can be /// provided; otherwise, <c>false</c>. /// </returns> public bool HasLineInfo() => true; /// <summary> /// Gets the current line number. /// </summary> /// <value> /// The current line number or 0 if no line information is available (for example, /// <see cref="JsonTextReader.HasLineInfo" /> returns <c>false</c>). /// </value> public int LineNumber { get { if (CurrentState == State.Start && LinePosition == 0 && TokenType != JsonToken.Comment) return 0; return _lineNumber; } } /// <summary> /// Gets the current line position. /// </summary> /// <value> /// The current line position or 0 if no line information is available (for example, /// <see cref="JsonTextReader.HasLineInfo" /> returns <c>false</c>). /// </value> public int LinePosition => CharPos - _lineStartPos; } }