diff --git a/src/Microsoft.OData.Core/Json/JsonReader.cs b/src/Microsoft.OData.Core/Json/JsonReader.cs index 4b9342c437..c4ca185fc8 100644 --- a/src/Microsoft.OData.Core/Json/JsonReader.cs +++ b/src/Microsoft.OData.Core/Json/JsonReader.cs @@ -232,7 +232,7 @@ public virtual object GetValue() } else { - this.nodeValue = this.ParseStringPrimitiveValue(out _); + this.nodeValue = GetCommonOrNewString(this.ParseStringPrimitiveValue(out _)); } } } @@ -564,10 +564,10 @@ public virtual Task GetValueAsync() else { // Quoted string (or single‑quoted for compat); fast-path if already completed. - ValueTask<(string Value, bool HasLeadingBackslash)> parseStringTask = this.ParseStringPrimitiveValueAsync(); + ValueTask<(ReadOnlyMemory Value, bool HasLeadingBackslash)> parseStringTask = this.ParseStringPrimitiveValueAsync(); if (parseStringTask.IsCompletedSuccessfully) { - this.nodeValue = parseStringTask.Result.Value; + this.nodeValue = GetCommonOrNewString(parseStringTask.Result.Value.Span); return Task.FromResult(this.nodeValue); } @@ -581,10 +581,10 @@ static async Task AwaitNullValueAsync(JsonReader thisParam, ValueTask AwaitStringValueAsync(JsonReader thisParam, ValueTask<(string Value, bool HasLeadingBackslash)> pendingParseStringTask) + static async Task AwaitStringValueAsync(JsonReader thisParam, ValueTask<(ReadOnlyMemory Value, bool HasLeadingBackslash)> pendingParseStringTask) { - (string Value, bool HasLeadingBackslash) result = await pendingParseStringTask.ConfigureAwait(false); - thisParam.nodeValue = result.Value; + (ReadOnlyMemory Value, bool HasLeadingBackslash) result = await pendingParseStringTask.ConfigureAwait(false); + thisParam.nodeValue = GetCommonOrNewString(result.Value.Span); return thisParam.nodeValue; } } @@ -976,18 +976,22 @@ private JsonNodeType ParseProperty() this.PushScope(ScopeType.Property); // Parse the name of the property - this.nodeValue = this.ParseName(); + ReadOnlySpan token = this.ParseName(); - if (string.IsNullOrEmpty((string)this.nodeValue)) + if (token.IsEmpty) { + this.nodeValue = string.Empty; + // The name can't be empty. - throw JsonReaderExtensions.CreateException(Error.Format(SRResources.JsonReader_InvalidPropertyNameOrUnexpectedComma, (string)this.nodeValue)); + throw JsonReaderExtensions.CreateException(Error.Format(SRResources.JsonReader_InvalidPropertyNameOrUnexpectedComma, this.nodeValue)); } + this.nodeValue = GetCommonOrNewString(token); + if (!this.SkipWhitespaces() || this.characterBuffer[this.tokenStartIndex] != ':') { // We need the colon character after the property name - throw JsonReaderExtensions.CreateException(Error.Format(SRResources.JsonReader_MissingColon, (string)this.nodeValue)); + throw JsonReaderExtensions.CreateException(Error.Format(SRResources.JsonReader_MissingColon, this.nodeValue)); } // Consume the colon. @@ -1008,7 +1012,7 @@ private JsonNodeType ParseProperty() /// Assumes that the current token position points to the opening quote. /// Note that the string parsing can never end with EndOfInput, since we're already seen the quote. /// So it can either return a string successfully or fail. - private string ParseStringPrimitiveValue() + private ReadOnlySpan ParseStringPrimitiveValue() { return this.ParseStringPrimitiveValue(out _); } @@ -1024,7 +1028,7 @@ private string ParseStringPrimitiveValue() /// Note that the string parsing can never end with EndOfInput, since we're already seen the quote. /// So it can either return a string successfully or fail. [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Splitting the function would make it hard to understand.")] - private string ParseStringPrimitiveValue(out bool hasLeadingBackslash) + private ReadOnlySpan ParseStringPrimitiveValue(out bool hasLeadingBackslash) { Debug.Assert(this.tokenStartIndex < this.storedCharacterCount, "At least the quote must be present."); @@ -1065,7 +1069,7 @@ private string ParseStringPrimitiveValue(out bool hasLeadingBackslash) } else { - this.stringValueBuilder.Length = 0; + this.stringValueBuilder.Clear(); } valueBuilder = this.stringValueBuilder; @@ -1118,14 +1122,7 @@ private string ParseStringPrimitiveValue(out bool hasLeadingBackslash) throw JsonReaderExtensions.CreateException(Error.Format(SRResources.JsonReader_UnrecognizedEscapeSequence, "\\uXXXX")); } - string unicodeHexValue = this.ConsumeTokenToString(4); - int characterValue; - if (!Int32.TryParse(unicodeHexValue, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out characterValue)) - { - throw JsonReaderExtensions.CreateException(Error.Format(SRResources.JsonReader_UnrecognizedEscapeSequence, "\\u" + unicodeHexValue)); - } - - valueBuilder.Append((char)characterValue); + valueBuilder.Append((char)this.ParseUnicodeHexValue()); break; default: throw JsonReaderExtensions.CreateException(Error.Format(SRResources.JsonReader_UnrecognizedEscapeSequence, "\\" + character)); @@ -1134,15 +1131,15 @@ private string ParseStringPrimitiveValue(out bool hasLeadingBackslash) else if (character == openingQuoteCharacter) { // Consume everything up to the quote character - string result; + ReadOnlySpan result; if (valueBuilder != null) { this.ConsumeTokenAppendToBuilder(valueBuilder, currentCharacterTokenRelativeIndex); - result = valueBuilder.ToString(); + result = valueBuilder.ToString().AsSpan(); } else { - result = this.ConsumeTokenToString(currentCharacterTokenRelativeIndex); + result = this.ConsumeTokenToSpan(currentCharacterTokenRelativeIndex); } Debug.Assert(this.characterBuffer[this.tokenStartIndex] == openingQuoteCharacter, "We should have consumed everything up to the quote character."); @@ -1173,11 +1170,11 @@ private object ParseNullPrimitiveValue() "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(); + ReadOnlySpan token = this.ParseName(); - if (!string.Equals(token, JsonConstants.JsonNullLiteral, StringComparison.Ordinal)) + if (!token.SequenceEqual(JsonConstants.JsonNullLiteral.AsSpan())) { - throw JsonReaderExtensions.CreateException(Error.Format(SRResources.JsonReader_UnexpectedToken, token)); + throw JsonReaderExtensions.CreateException(Error.Format(SRResources.JsonReader_UnexpectedToken, token.ToString())); } return null; @@ -1195,19 +1192,19 @@ private object ParseBooleanPrimitiveValue() "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(); + ReadOnlySpan token = this.ParseName(); - if (string.Equals(token, JsonConstants.JsonFalseLiteral, StringComparison.Ordinal)) + if (token.SequenceEqual(JsonConstants.JsonFalseLiteral.AsSpan())) { return false; } - if (string.Equals(token, JsonConstants.JsonTrueLiteral, StringComparison.Ordinal)) + if (token.SequenceEqual(JsonConstants.JsonTrueLiteral.AsSpan())) { return true; } - throw JsonReaderExtensions.CreateException(Error.Format(SRResources.JsonReader_UnexpectedToken, token)); + throw JsonReaderExtensions.CreateException(Error.Format(SRResources.JsonReader_UnexpectedToken, token.ToString())); } /// @@ -1238,20 +1235,20 @@ private object ParseNumberPrimitiveValue() } // We now have all the characters which belong to the number, consume it into a string. - string numberString = this.ConsumeTokenToString(currentCharacterTokenRelativeIndex); + ReadOnlySpan numberSpan = this.ConsumeTokenToSpan(currentCharacterTokenRelativeIndex); - return ParseNumericToken(numberString); + return ParseNumericToken(numberSpan); } /// /// Parses a name token. /// - /// The value of the name token. + /// The value of the name token. /// Name tokens are (for backward compat reasons) either /// - string value quoted with double quotes. /// - string value quoted with single quotes. /// - sequence of letters, digits, underscores and dollar signs (without quoted and in any order). - private string ParseName() + private ReadOnlySpan ParseName() { Debug.Assert(this.tokenStartIndex < this.storedCharacterCount, "Must have at least one character available."); @@ -1280,7 +1277,7 @@ private string ParseName() } while ((this.tokenStartIndex + currentCharacterTokenRelativeIndex) < this.storedCharacterCount || this.ReadInput()); - return this.ConsumeTokenToString(currentCharacterTokenRelativeIndex); + return this.ConsumeTokenToSpan(currentCharacterTokenRelativeIndex); } /// @@ -1379,8 +1376,7 @@ private int ReadChars(char[] chars, int offset, int maxLength) throw JsonReaderExtensions.CreateException(Error.Format(SRResources.JsonReader_UnrecognizedEscapeSequence, "\\uXXXX")); } - int characterValue = ParseUnicodeHexValue(); - character = (char)characterValue; + character = (char)this.ParseUnicodeHexValue(); // We are already positioned on the next character, so don't advance at the end advance = false; @@ -1517,7 +1513,26 @@ private bool SkipWhitespaces() /// /// The number of character after the token to make available. /// true if at least the required number of characters is available; false if end of input was reached. + [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool EnsureAvailableCharacters(int characterCountAfterTokenStart) + { + // Fast path: Check if we already have enough characters in the buffer. + if (this.tokenStartIndex + characterCountAfterTokenStart <= this.storedCharacterCount) + { + return true; + } + + // Slow path: We need to read more characters from the input. + return EnsureAvailableCharactersSlow(characterCountAfterTokenStart); + } + + /// + /// Ensures that a specified number of characters after the token start is available in the buffer. + /// + /// The number of character after the token to make available. + /// true if at least the required number of characters is available; false if end of input was reached. + [MethodImpl(MethodImplOptions.NoInlining)] + private bool EnsureAvailableCharactersSlow(int characterCountAfterTokenStart) { while (this.tokenStartIndex + characterCountAfterTokenStart > this.storedCharacterCount) { @@ -1532,16 +1547,34 @@ private bool EnsureAvailableCharacters(int characterCountAfterTokenStart) /// /// Consumes the characters starting at the start of the token - /// and returns them as a string. + /// and returns them as a . /// /// The number of characters after the token start to consume. /// The string value of the consumed token. - private string ConsumeTokenToString(int characterCount) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private ReadOnlySpan ConsumeTokenToSpan(int characterCount) + { + Debug.Assert(characterCount >= 0, "characterCount >= 0"); + Debug.Assert(this.tokenStartIndex + characterCount <= this.storedCharacterCount, "characterCount specified characters outside of the available range."); + + ReadOnlySpan result = this.characterBuffer.AsSpan(this.tokenStartIndex, characterCount); + this.tokenStartIndex += characterCount; + + return result; + } + + /// + /// Consumes the characters starting at the start of the token + /// and returns them as a . + /// + /// The number of characters after the token start to consume. + /// The string value of the consumed token. + private ReadOnlyMemory ConsumeTokenToMemory(int characterCount) { Debug.Assert(characterCount >= 0, "characterCount >= 0"); Debug.Assert(this.tokenStartIndex + characterCount <= this.storedCharacterCount, "characterCount specified characters outside of the available range."); - string result = new string(this.characterBuffer, this.tokenStartIndex, characterCount); + ReadOnlyMemory result = this.characterBuffer.AsMemory(this.tokenStartIndex, characterCount); this.tokenStartIndex += characterCount; return result; @@ -1737,18 +1770,22 @@ private async ValueTask ParsePropertyAsync() this.PushScope(ScopeType.Property); // Parse the name of the property - this.nodeValue = await this.ParseNameAsync().ConfigureAwait(false); + ReadOnlyMemory token = await this.ParseNameAsync().ConfigureAwait(false); - if (string.IsNullOrEmpty((string)this.nodeValue)) + if (token.Span.IsEmpty) { + this.nodeValue = string.Empty; + // The name can't be empty. - throw JsonReaderExtensions.CreateException(Error.Format(SRResources.JsonReader_InvalidPropertyNameOrUnexpectedComma, (string)this.nodeValue)); + throw JsonReaderExtensions.CreateException(Error.Format(SRResources.JsonReader_InvalidPropertyNameOrUnexpectedComma, this.nodeValue)); } + this.nodeValue = GetCommonOrNewString(token.Span); + if (!await this.SkipWhitespacesAsync().ConfigureAwait(false) || this.characterBuffer[this.tokenStartIndex] != ':') { // We need the colon character after the property name - throw JsonReaderExtensions.CreateException(Error.Format(SRResources.JsonReader_MissingColon, (string)this.nodeValue)); + throw JsonReaderExtensions.CreateException(Error.Format(SRResources.JsonReader_MissingColon, this.nodeValue)); } // Consume the colon. @@ -1765,8 +1802,6 @@ private async ValueTask ParsePropertyAsync() /// /// Asynchronously parses a primitive string value. /// - /// Set to true if the first character in the string was a backslash. This is used when parsing DateTime values - /// since they must start with an escaped slash character (\/). /// A task that represents the asynchronous operation. /// The value of the TResult parameter contains a tuple comprising of the value of the string primitive value /// and a value of true if the first character in the string has a backlash; otherwise false. @@ -1775,7 +1810,7 @@ private async ValueTask ParsePropertyAsync() /// Note that the string parsing can never end with EndOfInput, since we're already seen the quote. /// So it can either return a string successfully or fail. [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Splitting the function would make it hard to understand.")] - private async ValueTask<(string Value, bool HasLeadingBackslash)> ParseStringPrimitiveValueAsync() + private async ValueTask<(ReadOnlyMemory Value, bool HasLeadingBackslash)> ParseStringPrimitiveValueAsync() { Debug.Assert(this.tokenStartIndex < this.storedCharacterCount, "At least the quote must be present."); @@ -1816,7 +1851,7 @@ private async ValueTask ParsePropertyAsync() } else { - this.stringValueBuilder.Length = 0; + this.stringValueBuilder.Clear(); } valueBuilder = this.stringValueBuilder; @@ -1869,13 +1904,7 @@ private async ValueTask ParsePropertyAsync() throw JsonReaderExtensions.CreateException(Error.Format(SRResources.JsonReader_UnrecognizedEscapeSequence, "\\uXXXX")); } - string unicodeHexValue = this.ConsumeTokenToString(4); - int characterValue; - if (!Int32.TryParse(unicodeHexValue, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out characterValue)) - { - throw JsonReaderExtensions.CreateException(Error.Format(SRResources.JsonReader_UnrecognizedEscapeSequence, "\\u" + unicodeHexValue)); - } - + int characterValue = this.ParseUnicodeHexValue(); valueBuilder.Append((char)characterValue); break; default: @@ -1885,15 +1914,15 @@ private async ValueTask ParsePropertyAsync() else if (character == openingQuoteCharacter) { // Consume everything up to the quote character - string result; + ReadOnlyMemory result; if (valueBuilder != null) { this.ConsumeTokenAppendToBuilder(valueBuilder, currentCharacterTokenRelativeIndex); - result = valueBuilder.ToString(); + result = valueBuilder.ToString().AsMemory(); } else { - result = this.ConsumeTokenToString(currentCharacterTokenRelativeIndex); + result = this.ConsumeTokenToMemory(currentCharacterTokenRelativeIndex); } Debug.Assert(this.characterBuffer[this.tokenStartIndex] == openingQuoteCharacter, "We should have consumed everything up to the quote character."); @@ -1926,11 +1955,11 @@ private ValueTask ParseNullPrimitiveValueAsync() "The method should only be called when the 'n' character is the start of the token."); // We can call ParseNameAsync since we know the first character is 'n' and thus it won't be quoted. - ValueTask parseNameTask = this.ParseNameAsync(); + ValueTask> parseNameTask = this.ParseNameAsync(); if (parseNameTask.IsCompletedSuccessfully) { - string token = parseNameTask.Result; - if (!string.Equals(token, JsonConstants.JsonNullLiteral, StringComparison.Ordinal)) + ReadOnlyMemory token = parseNameTask.Result; + if (!token.Span.SequenceEqual(JsonConstants.JsonNullLiteral.AsSpan())) { // Return a faulted Task (rather than throw synchronously). return ValueTask.FromException( @@ -1943,11 +1972,11 @@ private ValueTask ParseNullPrimitiveValueAsync() // Slow path: allocate state machine only if we really have to await. return AwaitParseNameAsync(this, parseNameTask); - static async ValueTask AwaitParseNameAsync(JsonReader thisParam, ValueTask pendingParseNameTask) + static async ValueTask AwaitParseNameAsync(JsonReader thisParam, ValueTask> pendingParseNameTask) { - string token = await pendingParseNameTask.ConfigureAwait(false); + ReadOnlyMemory token = await pendingParseNameTask.ConfigureAwait(false); - if (!string.Equals(token, JsonConstants.JsonNullLiteral, StringComparison.Ordinal)) + if (!token.Span.SequenceEqual(JsonConstants.JsonNullLiteral.AsSpan())) { throw JsonReaderExtensions.CreateException(Error.Format(SRResources.JsonReader_UnexpectedToken, token)); } @@ -1969,17 +1998,17 @@ private ValueTask ParseBooleanPrimitiveValueAsync() "The method should only be called when the 't' or 'f' character is the start of the token."); // We can call ParseNameAsync since we know the first character is 't' or 'f' and thus it won't be quoted. - ValueTask parseNameTask = this.ParseNameAsync(); + ValueTask> parseNameTask = this.ParseNameAsync(); if (parseNameTask.IsCompletedSuccessfully) { - string token = parseNameTask.Result; + ReadOnlyMemory token = parseNameTask.Result; - if (string.Equals(token, JsonConstants.JsonFalseLiteral, StringComparison.Ordinal)) + if (token.Span.SequenceEqual(JsonConstants.JsonFalseLiteral.AsSpan())) { return ValueTask.FromResult(false); } - if (string.Equals(token, JsonConstants.JsonTrueLiteral, StringComparison.Ordinal)) + if (token.Span.SequenceEqual(JsonConstants.JsonTrueLiteral.AsSpan())) { return ValueTask.FromResult(true); } @@ -1991,16 +2020,16 @@ private ValueTask ParseBooleanPrimitiveValueAsync() // Slow path: allocate state machine only if we really have to await. return AwaitParseNameAsync(parseNameTask); - static async ValueTask AwaitParseNameAsync(ValueTask pendingParseNameTask) + static async ValueTask AwaitParseNameAsync(ValueTask> pendingParseNameTask) { - string token = await pendingParseNameTask.ConfigureAwait(false); + ReadOnlyMemory token = await pendingParseNameTask.ConfigureAwait(false); - if (string.Equals(token, JsonConstants.JsonFalseLiteral, StringComparison.Ordinal)) + if (token.Span.SequenceEqual(JsonConstants.JsonFalseLiteral.AsSpan())) { return false; } - if (string.Equals(token, JsonConstants.JsonTrueLiteral, StringComparison.Ordinal)) + if (token.Span.SequenceEqual(JsonConstants.JsonTrueLiteral.AsSpan())) { return true; } @@ -2057,9 +2086,9 @@ private ValueTask ParseNumberPrimitiveValueAsync() try { - string numberString = this.ConsumeTokenToString(currentCharacterTokenRelativeIndex); + ReadOnlyMemory numberMemory = this.ConsumeTokenToMemory(currentCharacterTokenRelativeIndex); - return ValueTask.FromResult(this.ParseNumericToken(numberString)); + return ValueTask.FromResult(this.ParseNumericToken(numberMemory.Span)); } catch (ODataException ex) { @@ -2086,8 +2115,8 @@ static async ValueTask AwaitParseNumberAsync(JsonReader thisParam, Value continue; } - string numString = thisParam.ConsumeTokenToString(relativeIndex); - return thisParam.ParseNumericToken(numString); + ReadOnlyMemory numMemory = thisParam.ConsumeTokenToMemory(relativeIndex); + return thisParam.ParseNumericToken(numMemory.Span); } // Need to read more input @@ -2096,8 +2125,8 @@ static async ValueTask AwaitParseNumberAsync(JsonReader thisParam, Value } // EOF: Parse what we have - string numberString = thisParam.ConsumeTokenToString(relativeIndex); - return thisParam.ParseNumericToken(numberString); + ReadOnlyMemory numberMemory = thisParam.ConsumeTokenToMemory(relativeIndex); + return thisParam.ParseNumericToken(numberMemory.Span); } } @@ -2110,19 +2139,19 @@ static async ValueTask AwaitParseNumberAsync(JsonReader thisParam, Value /// - string value quoted with double quotes. /// - string value quoted with single quotes. /// - sequence of letters, digits, underscores and dollar signs (without quoted and in any order). - private ValueTask ParseNameAsync() + private ValueTask> ParseNameAsync() { Debug.Assert(this.tokenStartIndex < this.storedCharacterCount, "Must have at least one character available."); char firstCharacter = this.characterBuffer[this.tokenStartIndex]; if ((firstCharacter == '"') || (firstCharacter == '\'')) { - ValueTask<(string Value, bool HasLeadingBackslash)> parseQuotedNameTask = this.ParseStringPrimitiveValueAsync(); + ValueTask<(ReadOnlyMemory Value, bool HasLeadingBackslash)> parseQuotedNameTask = this.ParseStringPrimitiveValueAsync(); if (parseQuotedNameTask.IsCompletedSuccessfully) { return ValueTask.FromResult(parseQuotedNameTask.Result.Value); } - + return AwaitParseQuotedNameAsync(this, parseQuotedNameTask); } @@ -2158,16 +2187,17 @@ private ValueTask ParseNameAsync() } } - return ValueTask.FromResult(this.ConsumeTokenToString(currentCharacterTokenRelativeIndex)); + ReadOnlyMemory nameMemory = this.ConsumeTokenToMemory(currentCharacterTokenRelativeIndex); + return ValueTask.FromResult(nameMemory); - static async ValueTask AwaitParseQuotedNameAsync(JsonReader thisParam, ValueTask<(string Value, bool HasLeadingBackslash)> pendingParseQuotedNameTask) + static async ValueTask> AwaitParseQuotedNameAsync(JsonReader thisParam, ValueTask<(ReadOnlyMemory Value, bool HasLeadingBackslash)> pendingParseQuotedNameTask) { - (string Value, bool HasLeadingBackslash) result = await pendingParseQuotedNameTask.ConfigureAwait(false); + (ReadOnlyMemory Value, bool HasLeadingBackslash) = await pendingParseQuotedNameTask.ConfigureAwait(false); - return result.Value; + return Value; } - static async ValueTask AwaitParseUnquotedNameAsync(JsonReader thisParam, ValueTask pendingReadInputTask, int relativeIndex) + static async ValueTask> AwaitParseUnquotedNameAsync(JsonReader thisParam, ValueTask pendingReadInputTask, int relativeIndex) { while (true) { @@ -2187,15 +2217,14 @@ static async ValueTask AwaitParseUnquotedNameAsync(JsonReader thisParam, continue; } - // Hit a non-name character. Return the accumulated name - return thisParam.ConsumeTokenToString(relativeIndex); + return thisParam.ConsumeTokenToMemory(relativeIndex); } pendingReadInputTask = thisParam.ReadInputAsync(); } // EOF: Return whatever we have accumulated so far - return thisParam.ConsumeTokenToString(relativeIndex); + return thisParam.ConsumeTokenToMemory(relativeIndex); } } @@ -2296,8 +2325,7 @@ private async Task ReadCharsAsync(char[] chars, int offset, int maxLength) throw JsonReaderExtensions.CreateException(Error.Format(SRResources.JsonReader_UnrecognizedEscapeSequence, "\\uXXXX")); } - int characterValue = ParseUnicodeHexValue(); - character = (char)characterValue; + character = (char)this.ParseUnicodeHexValue(); // We are already positioned on the next character, so don't advance at the end advance = false; @@ -2579,16 +2607,64 @@ private void CopyInputToBuffer() [MethodImpl(MethodImplOptions.AggressiveInlining)] private int ParseUnicodeHexValue() { - string unicodeHexValue = this.ConsumeTokenToString(4); - int characterValue; - if (!Int32.TryParse(unicodeHexValue, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out characterValue)) + Debug.Assert(this.tokenStartIndex + 4 <= this.storedCharacterCount, "4 specified characters outside of the available range."); + + char hexChar1 = this.characterBuffer[this.tokenStartIndex++]; + char hexChar2 = this.characterBuffer[this.tokenStartIndex++]; + char hexChar3 = this.characterBuffer[this.tokenStartIndex++]; + char hexChar4 = this.characterBuffer[this.tokenStartIndex++]; + + int characterValue = ParseFourHexDigits(hexChar1, hexChar2, hexChar3, hexChar4); + if (characterValue < 0) { - throw JsonReaderExtensions.CreateException(Error.Format(SRResources.JsonReader_UnrecognizedEscapeSequence, "\\u" + unicodeHexValue)); + throw JsonReaderExtensions.CreateException(Error.Format(SRResources.JsonReader_UnrecognizedEscapeSequence, "\\u" + new string(new char[] {hexChar1, hexChar2, hexChar3, hexChar4}))); } return characterValue; } + /// + /// Converts four hexadecimal characters to their integer value. + /// + /// The first hexadecimal character. + /// The second hexadecimal character. + /// The third hexadecimal character. + /// The fourth hexadecimal character. + /// + /// The integer value represented by the four hexadecimal characters, or -1 if any character is not a valid hexadecimal digit. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int ParseFourHexDigits(char hexChar1, char hexChar2, char hexChar3, char hexChar4) + { + int digit1 = HexCharToInt(hexChar1); + int digit2 = HexCharToInt(hexChar2); + int digit3 = HexCharToInt(hexChar3); + int digit4 = HexCharToInt(hexChar4); + + if ((digit1 | digit2 | digit3 | digit4) < 0) + { + return -1; + } + + return (digit1 << 12) | (digit2 << 8) | (digit3 << 4) | digit4; + } + + /// + /// Converts a single hexadecimal character to its integer value. + /// + /// The hexadecimal character to convert. + /// + /// The integer value of the hexadecimal character (0-15), or -1 if the character is not a valid hexadecimal digit. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int HexCharToInt(char hexChar) + { + if ((uint)(hexChar - '0') <= 9u) return hexChar - '0'; + if ((uint)(hexChar - 'A') <= 5u) return hexChar - 'A' + 10; + if ((uint)(hexChar - 'a') <= 5u) return hexChar - 'a' + 10; + return -1; + } + /// /// Determines if a given character is allowed in a JSON property name. /// @@ -2610,28 +2686,28 @@ private bool IsCharacterAllowedInPropertyName(char character) /// Canonical numeric token (no surrounding whitespace). /// Boxed int, decimal or double. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private object ParseNumericToken(string numberString) + private object ParseNumericToken(ReadOnlySpan numberSpan) { // 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 int intValue)) + if (Int32.TryParse(numberSpan, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out int intValue)) { return intValue; } // if it is not Ieee754Compatible, decimal will be parsed before double to keep precision - if (!this.isIeee754Compatible && Decimal.TryParse(numberString, NumberStyles.Number, NumberFormatInfo.InvariantInfo, out decimal decimalValue)) + if (!this.isIeee754Compatible && Decimal.TryParse(numberSpan, NumberStyles.Number, NumberFormatInfo.InvariantInfo, out decimal decimalValue)) { return decimalValue; } - if (Double.TryParse(numberString, NumberStyles.Float, NumberFormatInfo.InvariantInfo, out double doubleValue)) + if (Double.TryParse(numberSpan, NumberStyles.Float, NumberFormatInfo.InvariantInfo, out double doubleValue)) { return doubleValue; } - throw JsonReaderExtensions.CreateException(Error.Format(SRResources.JsonReader_InvalidNumberFormat, numberString)); + throw JsonReaderExtensions.CreateException(Error.Format(SRResources.JsonReader_InvalidNumberFormat, numberSpan.ToString())); } /// @@ -2650,6 +2726,33 @@ private static bool IsNumberChar(char ch) ch == '-' || ch == '+'; } + /// + /// Returns a shared string instance for common OData property names or values, otherwise returns a new string. + /// + /// + /// This method reduces memory usage by returning static instances for known property names or values. + /// For uncommon or unique strings, it returns a new string instance. + /// + /// A read-only span of characters representing the input string to process. + /// + /// A shared string instance if the input matches a predefined common value or property name; otherwise, a new string instance representing the input. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static string GetCommonOrNewString(ReadOnlySpan span) + { + if (span.IsEmpty) + { + return string.Empty; + } + + // For known property names, return static interned instances + if (ODataJsonUtils.TryGetMatchingCommonValueString(span, out string commonValue)) + { + return commonValue; + } + + return span.ToString(); + } /// /// Returns the result of a if already completed successfully; otherwise, awaits it. diff --git a/src/Microsoft.OData.Core/Json/ODataJsonConstants.cs b/src/Microsoft.OData.Core/Json/ODataJsonConstants.cs index 5d5af86e7a..10ec8d93ef 100644 --- a/src/Microsoft.OData.Core/Json/ODataJsonConstants.cs +++ b/src/Microsoft.OData.Core/Json/ODataJsonConstants.cs @@ -48,6 +48,9 @@ internal static class ODataJsonConstants /// The 'id' property name for the Json value property. internal const string ODataIdPropertyName = "id"; + /// The 'Id' property name for the Json value property. + internal const string ODataIdPascalCasePropertyName = "Id"; + /// The 'delta' property name for the Json value property. internal const string ODataDeltaPropertyName = "delta"; @@ -114,6 +117,9 @@ internal static class ODataJsonConstants /// The simplified OData Type property name. internal const string SimplifiedODataTypePropertyName = "@type"; + /// The OData Type property name. + internal const string ODataTypePropertyName = "type"; + /// The simplified Removed property name. internal const string SimplifiedODataRemovedPropertyName = "@removed"; diff --git a/src/Microsoft.OData.Core/Json/ODataJsonUtils.cs b/src/Microsoft.OData.Core/Json/ODataJsonUtils.cs index 0f8fd5898f..3adf313189 100644 --- a/src/Microsoft.OData.Core/Json/ODataJsonUtils.cs +++ b/src/Microsoft.OData.Core/Json/ODataJsonUtils.cs @@ -8,10 +8,10 @@ namespace Microsoft.OData.Json { using System; using System.Diagnostics; - using System.Diagnostics.CodeAnalysis; using System.Linq; - using Microsoft.OData.Metadata; + using System.Runtime.CompilerServices; using Microsoft.OData.Edm; + using Microsoft.OData.Metadata; /// /// Helper methods used by the OData reader for the Json format. @@ -152,5 +152,190 @@ internal static ODataOperation CreateODataOperation(Uri metadataDocumentUri, str operation.Metadata = GetAbsoluteUriFromMetadataReferencePropertyName(metadataDocumentUri, metadataReferencePropertyName); return operation; } + + /// + /// Attempts to match a given span of characters to a predefined set of common OData property names and returns the corresponding interned string if a match is found. + /// The method is intended to reduce memory usage by interning commonly used property names in OData payloads + /// + /// The span of characters to evaluate. This span is compared against known OData property names. + /// When this method returns, contains the interned string corresponding to the matched OData property name, if + /// the match is successful; otherwise, . + /// if the span matches one of the predefined OData property names; otherwise, . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool TryGetMatchingCommonValueString(ReadOnlySpan span, out string value) + { + value = null; + if (span.Length == 0) + return false; + + // Fast-path: check length and first char(s) before comparing full span + switch (span.Length) + { + case 2: // "id", "Id" + if (span[0] == 'i' && span[1] == 'd') + { + value = ODataJsonConstants.ODataIdPropertyName; + return true; + } + if (span[0] == 'I' && span[1] == 'd') + { + value = ODataJsonConstants.ODataIdPascalCasePropertyName; + return true; + } + break; + + case 3: // "url", "@id" + if (span[0] == 'u' && span[1] == 'r' && span[2] == 'l') + { + value = ODataJsonConstants.ODataServiceDocumentElementUrlName; + return true; + } + if (span[0] == '@' && span[1] == 'i' && span[2] == 'd') + { + value = ODataJsonConstants.SimplifiedODataIdPropertyName; + return true; + } + break; + + case 4: + // "null", "true", "name", "type" + if (span[0] == 'n' && span.SequenceEqual(ODataJsonConstants.ODataNullPropertyName.AsSpan())) + { + value = ODataJsonConstants.ODataNullPropertyName; + return true; + } + if (span[0] == 't' && span.SequenceEqual(ODataJsonConstants.ODataNullAnnotationTrueValue.AsSpan())) + { + value = ODataJsonConstants.ODataNullAnnotationTrueValue; + return true; + } + if (span[0] == 'n' && span.SequenceEqual(ODataJsonConstants.ODataServiceDocumentElementName.AsSpan())) + { + value = ODataJsonConstants.ODataServiceDocumentElementName; + return true; + } + if (span[0] == 't' && span.SequenceEqual(ODataJsonConstants.ODataTypePropertyName.AsSpan())) + { + value = ODataJsonConstants.ODataTypePropertyName; + return true; + } + break; + + case 5: + // "false", "value", "error", "delta", "@type" + if (span[0] == 'f' && span.SequenceEqual(JsonConstants.JsonFalseLiteral.AsSpan())) + { + value = JsonConstants.JsonFalseLiteral; + return true; + } + if (span[0] == 'v' && span.SequenceEqual(ODataJsonConstants.ODataValuePropertyName.AsSpan())) + { + value = ODataJsonConstants.ODataValuePropertyName; + return true; + } + if (span[0] == '@' && span[1] == 't' && span.SequenceEqual(ODataJsonConstants.SimplifiedODataTypePropertyName.AsSpan())) + { + value = ODataJsonConstants.SimplifiedODataTypePropertyName; + return true; + } + if (span[0] == 'd' && span.SequenceEqual(ODataJsonConstants.ODataDeltaPropertyName.AsSpan())) + { + value = ODataJsonConstants.ODataDeltaPropertyName; + return true; + } + if (span[0] == 'e' && span.SequenceEqual(ODataJsonConstants.ODataErrorPropertyName.AsSpan())) + { + value = ODataJsonConstants.ODataErrorPropertyName; + return true; + } + break; + + case 6: + // "reason", "source", "target" + if (span[0] == 'r' && span.SequenceEqual(ODataJsonConstants.ODataReasonPropertyName.AsSpan())) + { + value = ODataJsonConstants.ODataReasonPropertyName; + return true; + } + if (span[0] == 's' && span.SequenceEqual(ODataJsonConstants.ODataSourcePropertyName.AsSpan())) + { + value = ODataJsonConstants.ODataSourcePropertyName; + return true; + } + if (span[0] == 't' && span.SequenceEqual(ODataJsonConstants.ODataTargetPropertyName.AsSpan())) + { + value = ODataJsonConstants.ODataTargetPropertyName; + return true; + } + break; + + case 7: + // "changed", "deleted" + if (span[0] == 'c' && span.SequenceEqual(ODataJsonConstants.ODataReasonChangedValue.AsSpan())) + { + value = ODataJsonConstants.ODataReasonChangedValue; + return true; + } + if (span[0] == 'd' && span.SequenceEqual(ODataJsonConstants.ODataReasonDeletedValue.AsSpan())) + { + value = ODataJsonConstants.ODataReasonDeletedValue; + return true; + } + break; + + case 8: + // "@context", "@removed" + if (span[0] == '@' && span[1] == 'c' && span.SequenceEqual(ODataJsonConstants.SimplifiedODataContextPropertyName.AsSpan())) + { + value = ODataJsonConstants.SimplifiedODataContextPropertyName; + return true; + } + if (span[0] == '@' && span[1] == 'r' && span.SequenceEqual(ODataJsonConstants.SimplifiedODataRemovedPropertyName.AsSpan())) + { + value = ODataJsonConstants.SimplifiedODataRemovedPropertyName; + return true; + } + break; + + case 9: + // "@odata.id" + if (span[0] == '@' && span[7] == 'i' && span[8] == 'd' && span.SequenceEqual(ODataJsonConstants.PrefixedODataIdPropertyName.AsSpan())) + { + value = ODataJsonConstants.PrefixedODataIdPropertyName; + return true; + } + break; + + case 11: + // "@odata.type", "@odata.null" + if (span[0] == '@' && span[7] == 't' && span[8] == 'y' && span.SequenceEqual(ODataJsonConstants.PrefixedODataTypePropertyName.AsSpan())) + { + value = ODataJsonConstants.PrefixedODataTypePropertyName; + return true; + } + if (span[0] == '@' && span[7] == 'n' && span[8] == 'u' && span.SequenceEqual(ODataJsonConstants.PrefixedODataNullPropertyName.AsSpan())) + { + value = ODataJsonConstants.PrefixedODataNullPropertyName; + return true; + } + break; + + case 14: + // "@odata.context", "@odata.removed" + if (span[0] == '@' && span[7] == 'c' && span[8] == 'o' && span.SequenceEqual(ODataJsonConstants.PrefixedODataContextPropertyName.AsSpan())) + { + value = ODataJsonConstants.PrefixedODataContextPropertyName; + return true; + } + if (span[0] == '@' && span[7] == 'r' && span[8] == 'e' && span.SequenceEqual(ODataJsonConstants.PrefixedODataRemovedPropertyName.AsSpan())) + { + value = ODataJsonConstants.PrefixedODataRemovedPropertyName; + return true; + } + break; + } + + return false; + } } } diff --git a/test/UnitTests/Microsoft.OData.Core.Tests/Json/JsonReaderTests.cs b/test/UnitTests/Microsoft.OData.Core.Tests/Json/JsonReaderTests.cs index 542e6281c4..45dd5cc6af 100644 --- a/test/UnitTests/Microsoft.OData.Core.Tests/Json/JsonReaderTests.cs +++ b/test/UnitTests/Microsoft.OData.Core.Tests/Json/JsonReaderTests.cs @@ -149,6 +149,76 @@ public async Task ReadPrimitiveValue(string payload, Type expectedType, bool isI } } + [Theory] + [InlineData("{ \"greeting\": \"\\u0048\\u0065\\u006C\\u006C\\u006F\" }", "Hello")] + [InlineData("{ \"emoji\": \"\\uD83D\\uDE03\" }", "😃")] + [InlineData("{ \"chinese\": \"\\u6211\\u662F\\u4E2D\\u6587\" }", "我是中文")] + [InlineData("{ \"symbol\": \"\\u00A9\" }", "©")] + [InlineData("{ \"currency\": \"\\u20AC\" }", "€")] + [InlineData("{ \"greek\": \"\\u03A9\" }", "Ω")] + [InlineData("{ \"cyrillic\": \"\\u0416\" }", "Ж")] + [InlineData("{ \"arabic\": \"\\u0627\" }", "ا")] + [InlineData("{ \"hebrew\": \"\\u05D0\" }", "א")] + [InlineData("{ \"chinese\": \"\\u4E2D\" }", "中")] + [InlineData("{ \"hiragana\": \"\\u3042\" }", "あ")] + [InlineData("{ \"math\": \"\\u221E\" }", "∞")] + [InlineData("{ \"arrow\": \"\\u2192\" }", "→")] + [InlineData("{ \"box\": \"\\u25A0\" }", "■")] + [InlineData("{ \"music\": \"\\u266B\" }", "♫")] + [InlineData("{ \"latin\": \"\\u00E9\" }", "é")] + [InlineData("{ \"emoji\": \"\\uD83D\\uDE0A\" }", "😊")] + [InlineData("{ \"rocket\": \"\\uD83D\\uDE80\" }", "🚀")] + [InlineData("{ \"sentence\": \"\\u0048\\u0065\\u006C\\u006C\\u006F, \\u4E16\\u754C!\" }", "Hello, 世界!")] + [InlineData("{ \"word\": \"\\u4E16\\u754C\" }", "世界")] + [InlineData("{ \"word\": \"\\u03A9\\u006D\\u0065\\u0067\\u0061\" }", "Ωmega")] + [InlineData("{ \"word\": \"\\u0045\\u0073\\u0070\\u0061\\u00F1\\u0061\" }", "España")] + [InlineData("{ \"word\": \"\\u05E9\\u05DC\\u05D5\\u05DD\" }", "שלום")] + public async Task ReadUnicodeHexValueAsync(string payload, string expected) + { + using (var reader = new JsonReader(new StringReader(payload), isIeee754Compatible: false)) + { + await reader.ReadAsync(); // Read start of object + await reader.ReadAsync(); // Read property name - Data + await reader.ReadAsync(); // Position reader at the beginning of string + Assert.Equal(expected, await reader.GetValueAsync()); + } + } + + [Theory] + [InlineData("{ \"greeting\": \"\\u0048\\u0065\\u006C\\u006C\\u006F\" }", "Hello")] + [InlineData("{ \"emoji\": \"\\uD83D\\uDE03\" }", "😃")] + [InlineData("{ \"chinese\": \"\\u6211\\u662F\\u4E2D\\u6587\" }", "我是中文")] + [InlineData("{ \"symbol\": \"\\u00A9\" }", "©")] + [InlineData("{ \"currency\": \"\\u20AC\" }", "€")] + [InlineData("{ \"greek\": \"\\u03A9\" }", "Ω")] + [InlineData("{ \"cyrillic\": \"\\u0416\" }", "Ж")] + [InlineData("{ \"arabic\": \"\\u0627\" }", "ا")] + [InlineData("{ \"hebrew\": \"\\u05D0\" }", "א")] + [InlineData("{ \"chinese\": \"\\u4E2D\" }", "中")] + [InlineData("{ \"hiragana\": \"\\u3042\" }", "あ")] + [InlineData("{ \"math\": \"\\u221E\" }", "∞")] + [InlineData("{ \"arrow\": \"\\u2192\" }", "→")] + [InlineData("{ \"box\": \"\\u25A0\" }", "■")] + [InlineData("{ \"music\": \"\\u266B\" }", "♫")] + [InlineData("{ \"latin\": \"\\u00E9\" }", "é")] + [InlineData("{ \"emoji\": \"\\uD83D\\uDE0A\" }", "😊")] + [InlineData("{ \"rocket\": \"\\uD83D\\uDE80\" }", "🚀")] + [InlineData("{ \"sentence\": \"\\u0048\\u0065\\u006C\\u006C\\u006F, \\u4E16\\u754C!\" }", "Hello, 世界!")] + [InlineData("{ \"word\": \"\\u4E16\\u754C\" }", "世界")] + [InlineData("{ \"word\": \"\\u03A9\\u006D\\u0065\\u0067\\u0061\" }", "Ωmega")] + [InlineData("{ \"word\": \"\\u0045\\u0073\\u0070\\u0061\\u00F1\\u0061\" }", "España")] + [InlineData("{ \"word\": \"\\u05E9\\u05DC\\u05D5\\u05DD\" }", "שלום")] + public void ReadUnicodeHexValue(string payload, string expected) + { + using (var reader = new JsonReader(new StringReader(payload), isIeee754Compatible: false)) + { + reader.Read(); // Read start of object + reader.Read(); // Read property name - Data + reader.Read(); // Position reader at the beginning of string + Assert.Equal(expected, reader.GetValue()); + } + } + [Theory] [InlineData("{\"Data\":\"The \\r character\"}", "The \r character")] [InlineData("{\"Data\":\"The \\n character\"}", "The \n character")]