/// <summary> /// Parses a property name and the colon after it. /// </summary> /// <returns>The node type to report to the user.</returns> private JsonNodeType ParseProperty() { // Increase the count of values under the object (the number of properties). Debug.Assert(this.scopes.Count >= 1 && this.scopes.Peek().Type == ScopeType.Object, "Property can only occur in an object."); this.scopes.Peek().ValueCount++; this.PushScope(ScopeType.Property); // Parse the name of the property this.nodeValue = this.ParseName(); if (string.IsNullOrEmpty((string)this.nodeValue)) { // The name can't be empty. throw JsonReaderExtensions.CreateException(Strings.JsonReader_InvalidPropertyNameOrUnexpectedComma((string)this.nodeValue)); } if (!this.SkipWhitespaces() || this.characterBuffer[this.tokenStartIndex] != ':') { // We need the colon character after the property name throw JsonReaderExtensions.CreateException(Strings.JsonReader_MissingColon((string)this.nodeValue)); } // Consume the colon. Debug.Assert(this.characterBuffer[this.tokenStartIndex] == ':', "The above should verify that there's a colon."); this.tokenStartIndex++; return(JsonNodeType.Property); }
/// <summary> /// Parses a "value", that is an array, object or primitive value. /// </summary> /// <returns>The node type to report to the user.</returns> private JsonNodeType ParseValue() { Debug.Assert( this.tokenStartIndex < this.storedCharacterCount && !IsWhitespaceCharacter(this.characterBuffer[this.tokenStartIndex]), "The SkipWhitespaces wasn't called or it didn't correctly skip all whitespace characters from the input."); Debug.Assert(this.scopes.Count >= 1 && this.scopes.Peek().Type != ScopeType.Object, "Value can only occure at the root, in array or as a property value."); // Increase the count of values under the current scope. this.scopes.Peek().ValueCount++; char currentCharacter = this.characterBuffer[this.tokenStartIndex]; switch (currentCharacter) { case '{': // Start of object this.PushScope(ScopeType.Object); this.tokenStartIndex++; return(JsonNodeType.StartObject); case '[': // Start of array this.PushScope(ScopeType.Array); this.tokenStartIndex++; return(JsonNodeType.StartArray); case '"': case '\'': // String primitive value bool hasLeadingBackslash; this.nodeValue = this.ParseStringPrimitiveValue(out hasLeadingBackslash); break; case 'n': this.nodeValue = this.ParseNullPrimitiveValue(); break; case 't': case 'f': this.nodeValue = this.ParseBooleanPrimitiveValue(); break; default: // COMPAT 47: JSON number can start with dot. // The JSON spec doesn't allow numbers to start with ., but WCF DS does. We will follow the WCF DS behavior for compatibility. if (Char.IsDigit(currentCharacter) || (currentCharacter == '-') || (currentCharacter == '.')) { this.nodeValue = this.ParseNumberPrimitiveValue(); break; } else { // Unknown token - fail. throw JsonReaderExtensions.CreateException(Strings.JsonReader_UnrecognizedToken); } } this.TryPopPropertyScope(); return(JsonNodeType.PrimitiveValue); }
/// <summary> /// Skips over a JSON value (primitive, object or array). /// </summary> /// <param name="jsonReader">The <see cref="JsonReader"/> to read from.</param> /// <remarks> /// Pre-Condition: JsonNodeType.PrimitiveValue, JsonNodeType.StartArray or JsonNodeType.StartObject /// Post-Condition: JsonNodeType.PrimitiveValue, JsonNodeType.EndArray or JsonNodeType.EndObject /// </remarks> internal static void SkipValue(this IJsonReader jsonReader) { Debug.Assert(jsonReader != null, "jsonReader != null"); int depth = 0; do { switch (jsonReader.NodeType) { case JsonNodeType.StartArray: case JsonNodeType.StartObject: depth++; break; case JsonNodeType.EndArray: case JsonNodeType.EndObject: Debug.Assert(depth > 0, "Seen too many scope ends."); depth--; break; default: Debug.Assert( jsonReader.NodeType != JsonNodeType.EndOfInput, "We should not have reached end of input, since the scopes should be well formed. Otherwise JsonReader should have failed by now."); break; } }while (jsonReader.Read() && depth > 0); if (depth > 0) { // Not all open scopes were closed: // "Invalid JSON. Unexpected end of input was found in JSON content. Not all object and array scopes were closed." throw JsonReaderExtensions.CreateException(Strings.JsonReader_EndOfInputWithOpenScope); } }
/// <summary> /// Parses the number primitive values. /// </summary> /// <returns>Parse value to Int32, Decimal or Double. Otherwise throws.</returns> /// <remarks>Assumes that the current token position points to the first character of the number, so either digit, dot or dash.</remarks> private object ParseNumberPrimitiveValue() { Debug.Assert( this.tokenStartIndex < this.storedCharacterCount && (this.characterBuffer[this.tokenStartIndex] == '.' || this.characterBuffer[this.tokenStartIndex] == '-' || Char.IsDigit(this.characterBuffer[this.tokenStartIndex])), "The method should only be called when a digit, dash or dot character is the start of the token."); // Walk over all characters which might belong to the number // Skip the first one since we already verified it belongs to the number. int currentCharacterTokenRelativeIndex = 1; while ((this.tokenStartIndex + currentCharacterTokenRelativeIndex) < this.storedCharacterCount || this.ReadInput()) { char character = this.characterBuffer[this.tokenStartIndex + currentCharacterTokenRelativeIndex]; if (Char.IsDigit(character) || (character == '.') || (character == 'E') || (character == 'e') || (character == '-') || (character == '+')) { currentCharacterTokenRelativeIndex++; } else { break; } } // We now have all the characters which belong to the number, consume it into a string. string numberString = this.ConsumeTokenToString(currentCharacterTokenRelativeIndex); double doubleValue; int intValue; decimal decimalValue; // We will first try and convert the value to Int32. If it succeeds, use that. // And then, we will try Decimal, since it will lose precision while expected type is specified. // Otherwise, we will try and convert the value into a double. if (Int32.TryParse(numberString, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out intValue)) { return(intValue); } // if it is not Ieee754Compatible, decimal will be parsed before double to keep precision if (!isIeee754Compatible && Decimal.TryParse(numberString, NumberStyles.Number, NumberFormatInfo.InvariantInfo, out decimalValue)) { return(decimalValue); } if (Double.TryParse(numberString, NumberStyles.Float, NumberFormatInfo.InvariantInfo, out doubleValue)) { return(doubleValue); } throw JsonReaderExtensions.CreateException(Strings.JsonReader_InvalidNumberFormat(numberString)); }
/// <summary> /// Parses the null primitive value. /// </summary> /// <returns>Always returns null if successful. Otherwise throws.</returns> /// <remarks>Assumes that the current token position points to the 'n' character.</remarks> private object ParseNullPrimitiveValue() { Debug.Assert( this.tokenStartIndex < this.storedCharacterCount && this.characterBuffer[this.tokenStartIndex] == 'n', "The method should only be called when the 'n' character is the start of the token."); // We can call ParseName since we know the first character is 'n' and thus it won't be quoted. string token = this.ParseName(); if (!string.Equals(token, JsonConstants.JsonNullLiteral, StringComparison.Ordinal)) { throw JsonReaderExtensions.CreateException(Strings.JsonReader_UnexpectedToken(token)); } return(null); }
/// <summary> /// Called when end of input is reached. /// </summary> /// <returns>Always returns false, used for easy readability of the callers.</returns> private bool EndOfInput() { // We should be ending the input only with Root in the scope. if (this.scopes.Count > 1) { // Not all open scopes were closed. throw JsonReaderExtensions.CreateException(Strings.JsonReader_EndOfInputWithOpenScope); } Debug.Assert( this.scopes.Count > 0 && this.scopes.Peek().Type == ScopeType.Root && this.scopes.Peek().ValueCount <= 1, "The end of input should only occure with root at the top of the stack with zero or one value."); Debug.Assert(this.nodeValue == null, "The node value should have been reset to null."); this.nodeType = JsonNodeType.EndOfInput; return(false); }
/// <summary> /// Parses the true or false primitive values. /// </summary> /// <returns>true of false boolean value if successful. Otherwise throws.</returns> /// <remarks>Assumes that the current token position points to the 't' or 'f' character.</remarks> private object ParseBooleanPrimitiveValue() { Debug.Assert( this.tokenStartIndex < this.storedCharacterCount && (this.characterBuffer[this.tokenStartIndex] == 't' || this.characterBuffer[this.tokenStartIndex] == 'f'), "The method should only be called when the 't' or 'f' character is the start of the token."); // We can call ParseName since we know the first character is 't' or 'f' and thus it won't be quoted. string token = this.ParseName(); if (string.Equals(token, JsonConstants.JsonFalseLiteral, StringComparison.Ordinal)) { return(false); } if (string.Equals(token, JsonConstants.JsonTrueLiteral, StringComparison.Ordinal)) { return(true); } throw JsonReaderExtensions.CreateException(Strings.JsonReader_UnexpectedToken(token)); }
private string ParseStringPrimitiveValue(out bool hasLeadingBackslash) { Debug.Assert(this.tokenStartIndex < this.storedCharacterCount, "At least the quote must be present."); hasLeadingBackslash = false; // COMPAT 45: We allow both double and single quotes around a primitive string // Even though JSON spec only allows double quotes. char openingQuoteCharacter = this.characterBuffer[this.tokenStartIndex]; Debug.Assert(openingQuoteCharacter == '"' || openingQuoteCharacter == '\'', "The quote character must be the current character when this method is called."); // Consume the quote character this.tokenStartIndex++; // String builder to be used if we need to resolve escape sequences. StringBuilder valueBuilder = null; int currentCharacterTokenRelativeIndex = 0; while ((this.tokenStartIndex + currentCharacterTokenRelativeIndex) < this.storedCharacterCount || this.ReadInput()) { Debug.Assert((this.tokenStartIndex + currentCharacterTokenRelativeIndex) < this.storedCharacterCount, "ReadInput didn't read more data but returned true."); char character = this.characterBuffer[this.tokenStartIndex + currentCharacterTokenRelativeIndex]; if (character == '\\') { // If we're at the begining of the string // (means that relative token index must be 0 and we must not have consumed anything into our value builder yet) if (currentCharacterTokenRelativeIndex == 0 && valueBuilder == null) { hasLeadingBackslash = true; } // We will need the stringbuilder to resolve the escape sequences. if (valueBuilder == null) { if (this.stringValueBuilder == null) { this.stringValueBuilder = new StringBuilder(); } else { this.stringValueBuilder.Length = 0; } valueBuilder = this.stringValueBuilder; } // Append everything up to the \ character to the value. valueBuilder.Append(this.ConsumeTokenToString(currentCharacterTokenRelativeIndex)); currentCharacterTokenRelativeIndex = 0; Debug.Assert(this.characterBuffer[this.tokenStartIndex] == '\\', "We should have consumed everything up to the escape character."); // Escape sequence - we need at least two characters, the backslash and the one character after it. if (!this.EnsureAvailableCharacters(2)) { throw JsonReaderExtensions.CreateException(Strings.JsonReader_UnrecognizedEscapeSequence("\\")); } // To simplify the code, consume the character after the \ as well, since that is the start of the escape sequence. character = this.characterBuffer[this.tokenStartIndex + 1]; this.tokenStartIndex += 2; switch (character) { case 'b': valueBuilder.Append('\b'); break; case 'f': valueBuilder.Append('\f'); break; case 'n': valueBuilder.Append('\n'); break; case 'r': valueBuilder.Append('\r'); break; case 't': valueBuilder.Append('\t'); break; case '\\': case '\"': case '\'': case '/': valueBuilder.Append(character); break; case 'u': Debug.Assert(currentCharacterTokenRelativeIndex == 0, "The token should be starting at the first character after the \\u"); // We need 4 hex characters if (!this.EnsureAvailableCharacters(4)) { throw JsonReaderExtensions.CreateException(Strings.JsonReader_UnrecognizedEscapeSequence("\\uXXXX")); } string unicodeHexValue = this.ConsumeTokenToString(4); int characterValue; if (!Int32.TryParse(unicodeHexValue, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out characterValue)) { throw JsonReaderExtensions.CreateException(Strings.JsonReader_UnrecognizedEscapeSequence("\\u" + unicodeHexValue)); } valueBuilder.Append((char)characterValue); break; default: throw JsonReaderExtensions.CreateException(Strings.JsonReader_UnrecognizedEscapeSequence("\\" + character)); } } else if (character == openingQuoteCharacter) { // Consume everything up to the quote character string result = this.ConsumeTokenToString(currentCharacterTokenRelativeIndex); Debug.Assert(this.characterBuffer[this.tokenStartIndex] == openingQuoteCharacter, "We should have consumed everything up to the quote character."); // Consume the quote character as well. this.tokenStartIndex++; if (valueBuilder != null) { valueBuilder.Append(result); result = valueBuilder.ToString(); } return(result); } else { // Normal character, just skip over it - it will become part of the value as is. currentCharacterTokenRelativeIndex++; } } throw JsonReaderExtensions.CreateException(Strings.JsonReader_UnexpectedEndOfString); }
public virtual bool Read() { // Reset the node value. this.nodeValue = null; #if DEBUG // Reset the node type to None - so that we can verify that the Read method actually sets it. this.nodeType = JsonNodeType.None; #endif // Skip any whitespace characters. // This also makes sure that we have at least one non-whitespace character available. if (!this.SkipWhitespaces()) { return(this.EndOfInput()); } Debug.Assert( this.tokenStartIndex < this.storedCharacterCount && !IsWhitespaceCharacter(this.characterBuffer[this.tokenStartIndex]), "The SkipWhitespaces didn't correctly skip all whitespace characters from the input."); Scope currentScope = this.scopes.Peek(); bool commaFound = false; if (this.characterBuffer[this.tokenStartIndex] == ',') { commaFound = true; this.tokenStartIndex++; // Note that validity of the comma is verified below depending on the current scope. // Skip all whitespaces after comma. // Note that this causes "Unexpected EOF" error if the comma is the last thing in the input. // It might not be the best error message in certain cases, but it's still correct (a JSON payload can never end in comma). if (!this.SkipWhitespaces()) { return(this.EndOfInput()); } Debug.Assert( this.tokenStartIndex < this.storedCharacterCount && !IsWhitespaceCharacter(this.characterBuffer[this.tokenStartIndex]), "The SkipWhitespaces didn't correctly skip all whitespace characters from the input."); } switch (currentScope.Type) { case ScopeType.Root: if (commaFound) { throw JsonReaderExtensions.CreateException(Strings.JsonReader_UnexpectedComma(ScopeType.Root)); } if (currentScope.ValueCount > 0) { // We already found the top-level value, so fail throw JsonReaderExtensions.CreateException(Strings.JsonReader_MultipleTopLevelValues); } // We expect a "value" - start array, start object or primitive value this.nodeType = this.ParseValue(); break; case ScopeType.Array: if (commaFound && currentScope.ValueCount == 0) { throw JsonReaderExtensions.CreateException(Strings.JsonReader_UnexpectedComma(ScopeType.Array)); } // We might see end of array here if (this.characterBuffer[this.tokenStartIndex] == ']') { this.tokenStartIndex++; // End of array is only valid when there was no comma before it. if (commaFound) { throw JsonReaderExtensions.CreateException(Strings.JsonReader_UnexpectedComma(ScopeType.Array)); } this.PopScope(); this.nodeType = JsonNodeType.EndArray; break; } if (!commaFound && currentScope.ValueCount > 0) { throw JsonReaderExtensions.CreateException(Strings.JsonReader_MissingComma(ScopeType.Array)); } // We expect element which is a "value" - start array, start object or primitive value this.nodeType = this.ParseValue(); break; case ScopeType.Object: if (commaFound && currentScope.ValueCount == 0) { throw JsonReaderExtensions.CreateException(Strings.JsonReader_UnexpectedComma(ScopeType.Object)); } // We might see end of object here if (this.characterBuffer[this.tokenStartIndex] == '}') { this.tokenStartIndex++; // End of object is only valid when there was no comma before it. if (commaFound) { throw JsonReaderExtensions.CreateException(Strings.JsonReader_UnexpectedComma(ScopeType.Object)); } this.PopScope(); this.nodeType = JsonNodeType.EndObject; break; } else { if (!commaFound && currentScope.ValueCount > 0) { throw JsonReaderExtensions.CreateException(Strings.JsonReader_MissingComma(ScopeType.Object)); } // We expect a property here this.nodeType = this.ParseProperty(); break; } case ScopeType.Property: if (commaFound) { throw JsonReaderExtensions.CreateException(Strings.JsonReader_UnexpectedComma(ScopeType.Property)); } // We expect the property value, which is a "value" - start array, start object or primitive value this.nodeType = this.ParseValue(); break; default: throw JsonReaderExtensions.CreateException(Strings.General_InternalError(InternalErrorCodes.JsonReader_Read)); } Debug.Assert( this.nodeType != JsonNodeType.None && this.nodeType != JsonNodeType.EndOfInput, "Read should never go back to None and EndOfInput should be reported by directly returning."); return(true); }
/// <summary> /// Reads more characters from the input. /// </summary> /// <returns>true if more characters are available; false if end of input was reached.</returns> /// <remarks>This may move characters in the characterBuffer, so after this is called /// all indeces to the characterBuffer are invalid except for tokenStartIndex.</remarks> private bool ReadInput() { Debug.Assert(this.tokenStartIndex >= 0 && this.tokenStartIndex <= this.storedCharacterCount, "The token start is out of stored characters range."); if (this.endOfInputReached) { return(false); } // initialze the buffer if (this.characterBuffer == null) { this.characterBuffer = BufferUtils.RentFromBuffer(ArrayPool, InitialCharacterBufferSize); } Debug.Assert(this.storedCharacterCount <= this.characterBuffer.Length, "We can only store as many characters as fit into our buffer."); // If the buffer is empty (all characters were consumed from it), just start over. if (this.tokenStartIndex == this.storedCharacterCount) { this.tokenStartIndex = 0; this.storedCharacterCount = 0; } else if (this.storedCharacterCount == this.characterBuffer.Length) { // No more room in the buffer, move or grow the buffer. if (this.tokenStartIndex < this.characterBuffer.Length / 4) // Tested 1/2, 3/4 and 1/4, it seems 1/4 is better than other two. { // The entire buffer is full of unconsumed characters // We need to grow the buffer. Double the size of the buffer. if (this.characterBuffer.Length == int.MaxValue) { throw JsonReaderExtensions.CreateException(Strings.JsonReader_MaxBufferReached); } int newBufferSize = this.characterBuffer.Length * 2; newBufferSize = newBufferSize < 0 ? int.MaxValue : newBufferSize; // maybe overflow char[] newCharacterBuffer = BufferUtils.RentFromBuffer(ArrayPool, newBufferSize); // Copy the existing characters to the new buffer. Array.Copy(this.characterBuffer, this.tokenStartIndex, newCharacterBuffer, 0, this.storedCharacterCount - this.tokenStartIndex); this.storedCharacterCount = this.storedCharacterCount - this.tokenStartIndex; this.tokenStartIndex = 0; // And switch the buffers BufferUtils.ReturnToBuffer(ArrayPool, this.characterBuffer); this.characterBuffer = newCharacterBuffer; } else { // Some characters were consumed, we can just move them in the buffer // to get more room without allocating. Array.Copy(this.characterBuffer, this.tokenStartIndex, this.characterBuffer, 0, this.storedCharacterCount - this.tokenStartIndex); this.storedCharacterCount -= this.tokenStartIndex; this.tokenStartIndex = 0; } } Debug.Assert( this.storedCharacterCount < this.characterBuffer.Length, "We should have more room in the buffer by now."); // Read more characters from the input. // Use the Read method which returns any character as soon as it's available // we don't want to wait for the entire buffer to fill if the input doesn't have // the characters ready. int readCount = this.reader.Read( this.characterBuffer, this.storedCharacterCount, this.characterBuffer.Length - this.storedCharacterCount); if (readCount == 0) { // No more characters available, end of input. this.endOfInputReached = true; return(false); } this.storedCharacterCount += readCount; return(true); }
/// <summary> /// Skips over a JSON value (primitive, object or array), and append raw string to StringBuilder. /// </summary> /// <param name="jsonReader">The <see cref="JsonReader"/> to read from.</param> /// <param name="jsonRawValueStringBuilder">The StringBuilder to receive JSON raw string.</param> internal static void SkipValue(this IJsonReader jsonReader, StringBuilder jsonRawValueStringBuilder) { Debug.Assert(jsonReader != null, "jsonReader != null"); using (StringWriter stringWriter = new StringWriter(jsonRawValueStringBuilder, CultureInfo.InvariantCulture)) { JsonWriter jsonWriter = new JsonWriter(stringWriter, isIeee754Compatible: false); int depth = 0; do { switch (jsonReader.NodeType) { case JsonNodeType.PrimitiveValue: if (jsonReader.Value == null) { jsonWriter.WriteValue((string)null); } else { jsonWriter.WritePrimitiveValue(jsonReader.Value); } break; case JsonNodeType.StartArray: jsonWriter.StartArrayScope(); depth++; break; case JsonNodeType.StartObject: jsonWriter.StartObjectScope(); depth++; break; case JsonNodeType.EndArray: jsonWriter.EndArrayScope(); Debug.Assert(depth > 0, "Seen too many scope ends."); depth--; break; case JsonNodeType.EndObject: jsonWriter.EndObjectScope(); Debug.Assert(depth > 0, "Seen too many scope ends."); depth--; break; case JsonNodeType.Property: jsonWriter.WriteName(jsonReader.GetPropertyName()); break; default: Debug.Assert( jsonReader.NodeType != JsonNodeType.EndOfInput, "We should not have reached end of input, since the scopes should be well formed. Otherwise JsonReader should have failed by now."); break; } }while (jsonReader.Read() && depth > 0); if (depth > 0) { // Not all open scopes were closed: // "Invalid JSON. Unexpected end of input was found in JSON content. Not all object and array scopes were closed." throw JsonReaderExtensions.CreateException(Strings.JsonReader_EndOfInputWithOpenScope); } jsonWriter.Flush(); } }