diff --git a/src/Microsoft.OData.Core/Json/BufferingJsonReader.cs b/src/Microsoft.OData.Core/Json/BufferingJsonReader.cs index 3672eb17c0..a36f5b1690 100644 --- a/src/Microsoft.OData.Core/Json/BufferingJsonReader.cs +++ b/src/Microsoft.OData.Core/Json/BufferingJsonReader.cs @@ -11,7 +11,9 @@ namespace Microsoft.OData.Json using System; using System.Collections.Generic; using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; using System.IO; + using System.Runtime.CompilerServices; using System.Threading.Tasks; #endregion Namespaces @@ -21,6 +23,12 @@ namespace Microsoft.OData.Json /// internal class BufferingJsonReader : IJsonReader { + // Keep pools small; tune via benchmarks. + private const int MaxPooledNodesPerThread = 256; + + [ThreadStatic] + private static Stack? bufferedNodePool; + /// The (possibly empty) list of buffered nodes. /// This is a circular linked list where this field points to the first item of the list. protected BufferedNode bufferedNodesHead; @@ -178,7 +186,6 @@ internal bool IsBuffering /// Creates a stream for reading a stream value /// /// A Stream used to read a stream value - [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1825:Avoid zero-length array allocations.", Justification = "")] public virtual Stream CreateReadStream() { if (!this.isBuffering) @@ -285,19 +292,36 @@ public Task ReadAsync() /// /// A task that represents the asynchronous operation. /// true if the current value can be streamed; otherwise false. - public virtual async Task CanStreamAsync() + public virtual Task CanStreamAsync() { this.AssertAsynchronous(); - if (!this.isBuffering) + // If buffering, we already the value locally - refer to GetValueAsync implementation + if (this.isBuffering) { - return await this.innerReader.CanStreamAsync() - .ConfigureAwait(false); + Debug.Assert(this.currentBufferedNode != null, "this.currentBufferedNode != null"); + BufferedNode node = this.currentBufferedNode; + if (node.Value is string || node.Value == null || node.NodeType == JsonNodeType.StartArray || node.NodeType == JsonNodeType.StartObject) + { + return CachedTasks.True; + } + + return CachedTasks.False; } - object value = await this.GetValueAsync() - .ConfigureAwait(false); - return value is string || value == null || this.NodeType == JsonNodeType.StartArray || this.NodeType == JsonNodeType.StartObject; + // Non-buffering: defer to the inner reader + Task canStreamTask = this.innerReader.CanStreamAsync(); + if (canStreamTask.IsCompletedSuccessfully) + { + return canStreamTask; + } + + return AwaitCanStreamAsync(canStreamTask); + + static async Task AwaitCanStreamAsync(Task canStreamTask) + { + return await canStreamTask.ConfigureAwait(false); + } } /// @@ -305,7 +329,6 @@ public virtual async Task CanStreamAsync() /// /// A task that represents the asynchronous read operation. /// The value of the TResult parameter contains a used to read a stream value. - [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1825:Avoid zero-length array allocations.", Justification = "")] public virtual async Task CreateReadStreamAsync() { this.AssertAsynchronous(); @@ -382,7 +405,7 @@ internal void StartBuffering() if (this.bufferedNodesHead == null) { // capture the current state of the reader as the first item in the buffer (if there are none) - this.bufferedNodesHead = new BufferedNode(this.innerReader.NodeType, this.innerReader.GetValue()); + this.bufferedNodesHead = RentNode(this.innerReader.NodeType, this.innerReader.GetValue()); } else { @@ -482,7 +505,7 @@ internal bool StartBufferingAndTryToReadInStreamErrorPropertyValue(out ODataErro /// Asynchronously puts the reader into the state where it buffers read nodes. /// /// A task that represents the asynchronous operation. - internal async Task StartBufferingAsync() + internal Task StartBufferingAsync() { Debug.Assert(!this.isBuffering, "Buffering is already turned on. Must not call StartBuffering again."); this.AssertAsynchronous(); @@ -490,27 +513,44 @@ internal async Task StartBufferingAsync() if (this.bufferedNodesHead == null) { // capture the current state of the reader as the first item in the buffer (if there are none) - this.bufferedNodesHead = new BufferedNode( - this.innerReader.NodeType, - await this.innerReader.GetValueAsync().ConfigureAwait(false)); - } - else - { - this.removeOnNextRead = false; + Task getValueTask = this.innerReader.GetValueAsync(); + if (!getValueTask.IsCompletedSuccessfully) + { + return AwaitGetValueAndStartBufferingAsync(this, getValueTask); + } + + this.bufferedNodesHead = RentNode(this.innerReader.NodeType, getValueTask.Result); + CompleteStartBuffering(this); + return Task.CompletedTask; } - Debug.Assert(this.bufferedNodesHead != null, "Expected at least the current node in the buffer when starting buffering."); + // Already have a head node + this.removeOnNextRead = false; + CompleteStartBuffering(this); + return Task.CompletedTask; - // Set the currentBufferedNode to the first node in the list; this means every time we start buffering we reset the - // position of the current buffered node since in general we don't know how far ahead we have read before and thus don't - // want to blindly continuing to read. The model is that with every call to StartBufferingAsync you reset the position of the - // current node in the list and start reading through the buffer again. - if (this.currentBufferedNode == null) + static void CompleteStartBuffering(BufferingJsonReader thisParam) { - this.currentBufferedNode = this.bufferedNodesHead; + Debug.Assert(thisParam.bufferedNodesHead != null, "Expected at least the current node in the buffer when starting buffering."); + + // Set the currentBufferedNode to the first node in the list; this means every time we start buffering we reset the + // position of the current buffered node since in general we don't know how far ahead we have read before and thus don't + // want to blindly continuing to read. The model is that with every call to StartBufferingAsync you reset the position of the + // current node in the list and start reading through the buffer again. + if (thisParam.currentBufferedNode == null) + { + thisParam.currentBufferedNode = thisParam.bufferedNodesHead; + } + + thisParam.isBuffering = true; } - this.isBuffering = true; + static async Task AwaitGetValueAndStartBufferingAsync(BufferingJsonReader thisParam, Task getValueTask) + { + object value = await getValueTask.ConfigureAwait(false); + thisParam.bufferedNodesHead = RentNode(thisParam.innerReader.NodeType, value); + CompleteStartBuffering(thisParam); + } } /// @@ -520,7 +560,7 @@ internal async Task StartBufferingAsync() /// The value of the TResult parameter contains a tuple comprising of: /// 1). A value of true if the current value is an in-stream error value; otherwise false. /// 2). An instance that was read from the payload. - internal async Task<(bool IsReadSuccessfully, ODataError Error)> StartBufferingAndTryToReadInStreamErrorPropertyValueAsync() + internal async ValueTask<(bool IsReadSuccessfully, ODataError Error)> StartBufferingAndTryToReadInStreamErrorPropertyValueAsync() { this.AssertNotBuffering(); this.AssertAsynchronous(); @@ -549,7 +589,7 @@ await this.StartBufferingAsync() /// /// If the parsingInStreamError field is true, the method will read ahead for every StartObject node read from the input to check whether the JSON object /// represents an in-stream error. If so, it throws an . If false, this check will not happen. - /// This parsingInStremError field is set to true when trying to parse an in-stream error; in normal operation it is false. + /// This parsingInStreamError field is set to true when trying to parse an in-stream error; in normal operation it is false. /// protected bool ReadInternal() { @@ -678,9 +718,9 @@ protected virtual void ProcessObjectValue() /// /// If the parsingInStreamError field is true, the method will read ahead for every StartObject node read from the input to check whether the JSON object /// represents an in-stream error. If so, it throws an . If false, this check will not happen. - /// This parsingInStremError field is set to true when trying to parse an in-stream error; in normal operation it is false. + /// This parsingInStreamError field is set to true when trying to parse an in-stream error; in normal operation it is false. /// - protected async Task ReadInternalAsync() + protected Task ReadInternalAsync() { this.AssertAsynchronous(); @@ -689,11 +729,22 @@ protected async Task ReadInternalAsync() Debug.Assert(this.bufferedNodesHead != null, "If removeOnNextRead is true we must have at least one node in the buffer."); this.RemoveFirstNodeInBuffer(); - this.removeOnNextRead = false; } - bool result; + // Fast path: non-buffering replay from buffer + if (!this.isBuffering && this.bufferedNodesHead != null) + { + Debug.Assert(!this.parsingInStreamError, "!this.parsingInStreamError"); + + // Non-buffering read from the buffer + bool result = this.bufferedNodesHead.NodeType != JsonNodeType.EndOfInput; + this.removeOnNextRead = true; + + return result ? CachedTasks.True : CachedTasks.False; + } + + // Fast path: still walking inside buffer if (this.isBuffering) { Debug.Assert(this.currentBufferedNode != null, "this.currentBufferedNode != null"); @@ -701,46 +752,139 @@ protected async Task ReadInternalAsync() if (this.currentBufferedNode.Next != this.bufferedNodesHead) { this.currentBufferedNode = this.currentBufferedNode.Next; - result = true; + return CachedTasks.True; } - else if (this.parsingInStreamError) + } + + return ReadInternalLocalAsync(this); + + static Task ReadInternalLocalAsync(BufferingJsonReader thisParam) + { + if (thisParam.isBuffering) { - // Read more from the input stream and buffer it - result = await this.innerReader.ReadAsync() - .ConfigureAwait(false); - - // Add the new node to the end - this.AddNewNodeToTheEndOfBufferedNodesList( - this.innerReader.NodeType, - await this.innerReader.GetValueAsync().ConfigureAwait(false)); + Debug.Assert(thisParam.currentBufferedNode != null, "thisParam.currentBufferedNode != null"); + + if (thisParam.parsingInStreamError) + { + // Read more from the input stream and buffer it + Task innerReadTask = thisParam.innerReader.ReadAsync(); + if (!innerReadTask.IsCompletedSuccessfully) + { + return AwaitInnerReaderAsync(thisParam, innerReadTask); + } + + bool localResult = innerReadTask.Result; + if (!localResult) + { + return CachedTasks.False; + } + + return BufferValueIfNeededAsync(thisParam); + } + else + { + // Read the next node from the input stream and check + // whether it is an in-stream error + Task checkForInStreamErrorTask = thisParam.ReadNextAndCheckForInStreamErrorAsync(); + if (checkForInStreamErrorTask.IsCompletedSuccessfully) + { + return checkForInStreamErrorTask.Result ? CachedTasks.True : CachedTasks.False; + } + + return checkForInStreamErrorTask; + } } - else + + // Non-buffering + no buffered nodes yet + if (thisParam.bufferedNodesHead == null) { - // Read the next node from the input stream and check + // If parsingInStreamError nothing in the buffer; read from the base, + // else read the next node from the input stream and check // whether it is an in-stream error - result = await this.ReadNextAndCheckForInStreamErrorAsync() - .ConfigureAwait(false); + if (thisParam.parsingInStreamError) + { + Task readTask = thisParam.innerReader.ReadAsync(); + if (!readTask.IsCompletedSuccessfully) + { + return readTask; + } + + return readTask.Result ? CachedTasks.True : CachedTasks.False; + } + else + { + Task checkForInStreamErrorTask = thisParam.ReadNextAndCheckForInStreamErrorAsync(); + + if (checkForInStreamErrorTask.IsCompletedSuccessfully) + { + return checkForInStreamErrorTask.Result ? CachedTasks.True : CachedTasks.False; + } + + return checkForInStreamErrorTask; + } } + + // Should not reach here - buffered replay handled above + bool result = thisParam.bufferedNodesHead.NodeType != JsonNodeType.EndOfInput; + thisParam.removeOnNextRead = true; + + return result ? CachedTasks.True : CachedTasks.False; } - else if (this.bufferedNodesHead == null) + + static Task BufferValueIfNeededAsync(BufferingJsonReader thisParam) { - // If parsingInStreamError nothing in the buffer; read from the base, - // else read the next node from the input stream and check - // whether it is an in-stream error - result = this.parsingInStreamError ? - await this.innerReader.ReadAsync().ConfigureAwait(false) : - await this.ReadNextAndCheckForInStreamErrorAsync().ConfigureAwait(false); + object value = null; + JsonNodeType localNodeType = thisParam.innerReader.NodeType; + // GetValueAsync returns null for everything other than primitive value and property nameof + // This check should help us avoid pointless calls + // TODO: Verify correctness of this check + if (localNodeType == JsonNodeType.PrimitiveValue || localNodeType == JsonNodeType.Property) + { + Task getValueTask = thisParam.innerReader.GetValueAsync(); + if (!getValueTask.IsCompletedSuccessfully) + { + return AwaitGetValueAsync(thisParam, getValueTask); + } + + value = getValueTask.Result; + } + + thisParam.AddNewNodeToTheEndOfBufferedNodesList(thisParam.innerReader.NodeType, value); + + return CachedTasks.True; } - else + + static async Task AwaitInnerReaderAsync(BufferingJsonReader thisParam, Task innerReadTask) { - Debug.Assert(!this.parsingInStreamError, "!this.parsingInStreamError"); + if (!await innerReadTask.ConfigureAwait(false)) + { + return false; + } - // Non-buffering read from the buffer - result = this.bufferedNodesHead.NodeType != JsonNodeType.EndOfInput; - this.removeOnNextRead = true; + object value = null; + JsonNodeType localNodeType = thisParam.innerReader.NodeType; + // GetValueAsync returns null for everything other than primitive value and property nameof + // This check should help us avoid pointless calls + // TODO: Verify correctness of this check + if (localNodeType == JsonNodeType.PrimitiveValue || localNodeType == JsonNodeType.Property) + { + value = await thisParam.innerReader.GetValueAsync().ConfigureAwait(false); + } + + // Add new node to the end + thisParam.AddNewNodeToTheEndOfBufferedNodesList(thisParam.innerReader.NodeType, value); + + return true; } - return result; + static async Task AwaitGetValueAsync(BufferingJsonReader thisParam, Task getValueTask) + { + object value = await getValueTask.ConfigureAwait(false); + // Add new node to the end + thisParam.AddNewNodeToTheEndOfBufferedNodesList(thisParam.innerReader.NodeType, value); + + return true; + } } /// @@ -753,7 +897,7 @@ await this.innerReader.ReadAsync().ConfigureAwait(false) : /// once it returns the reader will be returned to the position before the method was called. /// The reader is always positioned on a start object when this method is called. /// - protected virtual async Task ProcessObjectValueAsync() + protected virtual Task ProcessObjectValueAsync() { Debug.Assert(this.currentBufferedNode.NodeType == JsonNodeType.StartObject, "this.currentBufferedNode.NodeType == JsonNodeType.StartObject"); Debug.Assert(this.parsingInStreamError, "this.parsingInStreamError"); @@ -763,50 +907,124 @@ protected virtual async Task ProcessObjectValueAsync() // Only check for in-stream errors if the buffering reader is told to do so if (this.DisableInStreamErrorDetection) { - return; + return Task.CompletedTask; } // Move to the first property of the potential error object (or the EndObject node if no properties exist) - await this.ReadInternalAsync() - .ConfigureAwait(false); + Task readInternalTask = this.ReadInternalAsync(); + if (!readInternalTask.IsCompletedSuccessfully) + { + return AwaitReadInternalAsync(this, readInternalTask); + } + + // Fast path: ReadInternalAsync completed synchronously + return this.DetectAndTryReadInStreamErrorAsync(); + + static async Task AwaitReadInternalAsync(BufferingJsonReader thisParam, Task readInternalTask) + { + await readInternalTask.ConfigureAwait(false); + await thisParam.DetectAndTryReadInStreamErrorAsync(); + } + } + /// + /// Checks the current buffered object (already positioned at its first child) + /// for exactly one matching in-stream error property and parses it. + /// + /// A faulted task on success (error detected); otherwise a completed task. + private Task DetectAndTryReadInStreamErrorAsync() + { // We only consider this to be an in-stream error if the object has a single 'error' property - bool errorPropertyFound = false; - ODataError parsedError = null; + if (this.currentBufferedNode.NodeType != JsonNodeType.Property) + { + return Task.CompletedTask; + } - while (this.currentBufferedNode.NodeType == JsonNodeType.Property) + // NOTE: The JSON reader already ensures that the value of a property node (which is the name of the property) is a string + // First (and only candidate) property must match the in-stream error + string propertyName = (string)this.currentBufferedNode.Value; + if (string.CompareOrdinal(this.inStreamErrorPropertyName, propertyName) != 0) { - // NOTE: The JSON reader already ensures that the value of a property node (which is the name of the property) is a string - string propertyName = (string)this.currentBufferedNode.Value; + return Task.CompletedTask; + } - // If we found any other property than the expected in-stream error property, we don't treat it as an in-stream error - if (string.CompareOrdinal(this.inStreamErrorPropertyName, propertyName) != 0 || errorPropertyFound) - { - return; - } + // Advance to property value and read error object + ValueTask<(bool IsReadSuccessfully, ODataError Error)> tryReadInStreamErrorTask = this.TryReadInStreamErrorAsync(); + if (!tryReadInStreamErrorTask.IsCompletedSuccessfully) + { + return AwaitTryReadInStreamErrorAsync(this, tryReadInStreamErrorTask); + } - errorPropertyFound = true; + (bool isReadSuccessfully, ODataError odataError) = tryReadInStreamErrorTask.Result; + if (!isReadSuccessfully) + { + // This means we thought we saw an in-stream error, but then + // we didn't see an intelligible error object, so we give up on reading the in-stream error + // and return. We will fail later in some other way. This payload is totally messed up. + return Task.CompletedTask; + } - // Position the reader over the property value - await this.ReadInternalAsync() - .ConfigureAwait(false); + // If we found any other property than the expected in-stream error property, we don't treat it as an in-stream error + // This check preserves same semantic as the while loop in the synchronous ProcessObjectValue + if (this.currentBufferedNode.NodeType == JsonNodeType.Property) + { + return Task.CompletedTask; + } - (bool isReadSuccessfully, ODataError error) = await this.TryReadInStreamErrorPropertyValueAsync() - .ConfigureAwait(false); + // Single in-stream error property, valid error object, return faulted Task + return Task.FromException(new ODataErrorException(odataError)); + + static async Task AwaitTryReadInStreamErrorAsync( + BufferingJsonReader thisParam, + ValueTask<(bool IsReadSuccessfully, ODataError Error)> pendingTryReadInStreamErrorTask) + { + (bool isReadSuccessfully, ODataError odataError) = await pendingTryReadInStreamErrorTask.ConfigureAwait(false); if (!isReadSuccessfully) { - // This means we thought we saw an in-stream error, but then - // we didn't see an intelligible error object, so we give up on reading the in-stream error - // and return. We will fail later in some other way. This payload is totally messed up. return; } - parsedError = error; + // Additional property invalidates the object as in-stream error + if (thisParam.currentBufferedNode.NodeType == JsonNodeType.Property) + { + return; + } + + throw new ODataErrorException(odataError); + } + } + + /// + /// Advances from the error property name to its value and attempts to parse + /// the value as an OData error object. + /// + /// + /// A task that represents the asynchronous read operation. + /// The value of the TResult parameter contains a success flag and the parsed error. + /// + private ValueTask<(bool IsReadSuccessfully, ODataError Error)> TryReadInStreamErrorAsync() + { + // Position the reader over the property value + Task readInternalTask = this.ReadInternalAsync(); + if (!readInternalTask.IsCompletedSuccessfully) + { + return AwaitReadInternalAsync(this, readInternalTask); + } + + // Read error object + ValueTask<(bool IsReadSuccessfully, ODataError Error)> tryReadInStreamErrorTask = this.TryReadInStreamErrorPropertyValueAsync(); + if (!tryReadInStreamErrorTask.IsCompletedSuccessfully) + { + return tryReadInStreamErrorTask; // Caller will await } - if (errorPropertyFound) + return ValueTask.FromResult(tryReadInStreamErrorTask.Result); + + static async ValueTask<(bool, ODataError)> AwaitReadInternalAsync(BufferingJsonReader thisParam, Task pendingReadInternalTask) { - throw new ODataErrorException(parsedError); + await pendingReadInternalTask.ConfigureAwait(false); + + return await thisParam.TryReadInStreamErrorPropertyValueAsync().ConfigureAwait(false); } } @@ -1067,8 +1285,8 @@ private bool TryReadErrorDetail(out ODataErrorDetail detail) { case JsonConstants.ODataErrorCodeName: if (!ODataJsonReaderUtils.ErrorPropertyNotFound( - ref propertiesFoundBitmask, - ODataJsonReaderUtils.ErrorPropertyBitMask.Code)) + ref propertiesFoundBitmask, + ODataJsonReaderUtils.ErrorPropertyBitMask.Code)) { return false; } @@ -1087,8 +1305,8 @@ private bool TryReadErrorDetail(out ODataErrorDetail detail) case JsonConstants.ODataErrorTargetName: if (!ODataJsonReaderUtils.ErrorPropertyNotFound( - ref propertiesFoundBitmask, - ODataJsonReaderUtils.ErrorPropertyBitMask.Target)) + ref propertiesFoundBitmask, + ODataJsonReaderUtils.ErrorPropertyBitMask.Target)) { return false; } @@ -1107,8 +1325,8 @@ private bool TryReadErrorDetail(out ODataErrorDetail detail) case JsonConstants.ODataErrorMessageName: if (!ODataJsonReaderUtils.ErrorPropertyNotFound( - ref propertiesFoundBitmask, - ODataJsonReaderUtils.ErrorPropertyBitMask.MessageValue)) + ref propertiesFoundBitmask, + ODataJsonReaderUtils.ErrorPropertyBitMask.MessageValue)) { return false; } @@ -1298,25 +1516,7 @@ private void SkipValueInternal() int depth = 0; do { - switch (this.currentBufferedNode.NodeType) - { - case JsonNodeType.PrimitiveValue: - break; - 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( - this.currentBufferedNode.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; - } + depth = AdjustDepth(depth, this.currentBufferedNode.NodeType); this.ReadInternal(); } @@ -1328,6 +1528,8 @@ private void SkipValueInternal() /// private void RemoveFirstNodeInBuffer() { + BufferedNode firstNodeInBuffer = this.bufferedNodesHead; + if (this.bufferedNodesHead.Next == this.bufferedNodesHead) { Debug.Assert(this.bufferedNodesHead.Previous == this.bufferedNodesHead, "The linked list is corrupted."); @@ -1339,6 +1541,8 @@ private void RemoveFirstNodeInBuffer() this.bufferedNodesHead.Next.Previous = this.bufferedNodesHead.Previous; this.bufferedNodesHead = this.bufferedNodesHead.Next; } + + ReturnNode(firstNodeInBuffer); } /// @@ -1347,7 +1551,9 @@ private void RemoveFirstNodeInBuffer() /// /// A task that represents the asynchronous read operation. /// The value of the TResult parameter contains true if a new node was found, or false if end of input was reached. - private async Task ReadNextAndCheckForInStreamErrorAsync() + [SuppressMessage("Design", "CA1031:Do not catch general exception types", + Justification = "We must (1) reset parsingInStreamError on synchronous failure, and (2) preserve the contract of returning a faulted Task instead of throwing synchronously.")] + private Task ReadNextAndCheckForInStreamErrorAsync() { Debug.Assert(!this.parsingInStreamError, "!this.parsingInStreamError"); this.AssertAsynchronous(); @@ -1356,47 +1562,173 @@ private async Task ReadNextAndCheckForInStreamErrorAsync() try { - // Read the next node in the current reader mode (buffering or non-buffering) - bool result = await this.ReadInternalAsync() - .ConfigureAwait(false); + Task readInternalTask = ReadInternalAsync(); + if (!readInternalTask.IsCompletedSuccessfully) + { + // Asynchronous completion path; continuation will reset parsingInStreamError and fault the Task if needed + return AwaitReadInternalAsync(this, readInternalTask); + } - if (this.innerReader.NodeType == JsonNodeType.StartObject) + // Synchronous completion path + return ContinueAfterReadInternalAsync(this, readInternalTask.Result); + } + catch (Exception ex) + { + this.parsingInStreamError = false; + return Task.FromException(ex); + } + + static Task ContinueAfterReadInternalAsync(BufferingJsonReader thisParam, bool innerResult) + { + // If not StartObject we can finish immediately. + if (thisParam.innerReader.NodeType != JsonNodeType.StartObject) { - // If we find a StartObject node we have to read ahead and check whether this - // JSON object represents an in-stream error. If we are currently in buffering - // mode remember the current position in the buffer; otherwise start buffering. - bool wasBuffering = this.isBuffering; - BufferedNode storedPosition = null; - if (this.isBuffering) - { - storedPosition = this.currentBufferedNode; - } - else - { - await this.StartBufferingAsync() - .ConfigureAwait(false); - } + thisParam.parsingInStreamError = false; + return innerResult ? CachedTasks.True : CachedTasks.False; + } - await this.ProcessObjectValueAsync() - .ConfigureAwait(false); + // If we find a StartObject node we have to read ahead and check whether this + // JSON object represents an in-stream error. + Task checkObjectForInStreamErrorTask; - // Reset the reader state to non-buffering or to the previously - // backed up position in the buffer. - if (wasBuffering) - { - this.currentBufferedNode = storedPosition; - } - else - { - this.StopBuffering(); - } + try + { + checkObjectForInStreamErrorTask = thisParam.ReadObjectAndCheckForInStreamErrorAsync(); + } + catch (Exception ex) + { + thisParam.parsingInStreamError = false; + return Task.FromException(ex); } - return result; + if (checkObjectForInStreamErrorTask.IsCompletedSuccessfully) + { + // Completed synchronously + thisParam.parsingInStreamError = false; + return innerResult ? CachedTasks.True : CachedTasks.False; + } + + // Asynchronous completion path; continuation will reset this.parsingInStreamError and fault the Task if needed + return AwaitReadObjectAndCheckForInStreamErrorAsync(thisParam, checkObjectForInStreamErrorTask, innerResult); } - finally + + static async Task AwaitReadInternalAsync(BufferingJsonReader thisParam, Task pendingReadInternalTask) { - this.parsingInStreamError = false; + // Any exception propagates as faulted task since method is async + bool innerResult; + + try + { + innerResult = await pendingReadInternalTask.ConfigureAwait(false); + } + catch + { + thisParam.parsingInStreamError = false; + throw; + } + + // May return a completed Task or an async path + bool finalResult = await ContinueAfterReadInternalAsync(thisParam, innerResult).ConfigureAwait(false); + Debug.Assert(!thisParam.parsingInStreamError, "parsingInStreamError must be false after ContinueAfterReadInternalAsync."); + + return finalResult; + } + + static async Task AwaitReadObjectAndCheckForInStreamErrorAsync(BufferingJsonReader thisParam, Task pendingCheckObjectForInStreamErrorTask, bool result) + { + // Any exception propagates as faulted task since method is async + try + { + await pendingCheckObjectForInStreamErrorTask.ConfigureAwait(false); + return result; + } + finally + { + thisParam.parsingInStreamError = false; + } + } + } + + /// + /// Given the inner reader just produced a StartObject, buffer it (if needed), + /// probe it for a single in-stream error property, and restore the original buffered position. + /// + /// A completed or faulted task that represents the asynchronous read operation. + private Task ReadObjectAndCheckForInStreamErrorAsync() + { + Debug.Assert( + this.innerReader.NodeType == JsonNodeType.StartObject, + "this.innerReader.NodeType == JsonNodeType.StartObject"); + + BufferedNode storedPosition = null; + + // If we are currently in buffering mode remember the current position in the buffer; + // otherwise start buffering. + if (this.isBuffering) + { + // Already buffering; remember position - restore on success + storedPosition = this.currentBufferedNode; + + Task processObjectValueTask = this.ProcessObjectValueAsync(); + if (processObjectValueTask.IsCompletedSuccessfully) + { + // Completed synchronously + this.currentBufferedNode = storedPosition; + return Task.CompletedTask; + } + + return AwaitProcessObjectValueAsync(this, storedPosition, processObjectValueTask); + } + else + { + // Need to start buffering first + Task startBufferingTask = this.StartBufferingAsync(); + if (!startBufferingTask.IsCompletedSuccessfully) + { + return AwaitStartBufferingThenProcessObjectValueAsync(this, startBufferingTask); + } + + // StartBufferingAsync completed synchronously + Task processObjectValueTask = this.ProcessObjectValueAsync(); + if (processObjectValueTask.IsCompletedSuccessfully) + { + // Fast path + this.StopBuffering(); + return Task.CompletedTask; + } + + return AwaitProcessObjectValueThenStopBufferingAsync(this, processObjectValueTask); + } + + // We were already buffering. Only restore position on success; on fault keep the advanced position - preserve semantics before refactor + static async Task AwaitProcessObjectValueAsync( + BufferingJsonReader thisParam, + BufferedNode storedPosition, + Task pendingProcessObjectValueTask) + { + await pendingProcessObjectValueTask.ConfigureAwait(false); + thisParam.currentBufferedNode = storedPosition; + } + + // If start buffering faults, we propagate and leave buffering turned on - preserve semantics before refactor + static async Task AwaitStartBufferingThenProcessObjectValueAsync(BufferingJsonReader thisParam, Task pendingStartBufferingTask) + { + await pendingStartBufferingTask.ConfigureAwait(false); + + Task processObjectValueTask = thisParam.ProcessObjectValueAsync(); + if (!processObjectValueTask.IsCompletedSuccessfully) + { + await processObjectValueTask.ConfigureAwait(false); + } + + // On success stop buffering + thisParam.StopBuffering(); + } + + static async Task AwaitProcessObjectValueThenStopBufferingAsync(BufferingJsonReader thisParam, Task pendingProcessObjectValueTask) + { + await pendingProcessObjectValueTask.ConfigureAwait(false); + thisParam.StopBuffering(); } } @@ -1408,7 +1740,7 @@ await this.ProcessObjectValueAsync() /// The value of the TResult parameter contains a tuple comprising of: /// 1). A value of true if an instance that was read; otherwise false. /// 2). An instance that was read from the reader or null if none could be read. - private async Task<(bool IsReadSuccessfully, ODataError Error)> TryReadInStreamErrorPropertyValueAsync() + private async ValueTask<(bool IsReadSuccessfully, ODataError Error)> TryReadInStreamErrorPropertyValueAsync() { Debug.Assert(this.parsingInStreamError, "this.parsingInStreamError"); this.AssertBuffering(); @@ -1425,8 +1757,6 @@ await this.ReadInternalAsync() .ConfigureAwait(false); ODataError error = new ODataError(); - bool isReadSuccessfully; - string propertyValue; // We expect one of the supported properties for the value (or EndObject) ODataJsonReaderUtils.ErrorPropertyBitMask propertiesFoundBitmask = ODataJsonReaderUtils.ErrorPropertyBitMask.None; @@ -1437,94 +1767,92 @@ await this.ReadInternalAsync() switch (propertyName) { case JsonConstants.ODataErrorCodeName: - if (!ODataJsonReaderUtils.ErrorPropertyNotFound( - ref propertiesFoundBitmask, - ODataJsonReaderUtils.ErrorPropertyBitMask.Code)) { - return (false, error); - } + (bool isReadSuccessfully, string propertyValue) = await this.ValidateAndTryReadErrorStringPropertyValueAsync( + ODataJsonReaderUtils.ErrorPropertyBitMask.Code, + ref propertiesFoundBitmask).ConfigureAwait(false); - (isReadSuccessfully, propertyValue) = await this.TryReadErrorStringPropertyValueAsync() - .ConfigureAwait(false); - if (!isReadSuccessfully) - { - return (false, error); - } + if (!isReadSuccessfully) + { + return (false, error); + } - error.Code = propertyValue; - break; + error.Code = propertyValue; - case JsonConstants.ODataErrorMessageName: - if (!ODataJsonReaderUtils.ErrorPropertyNotFound( - ref propertiesFoundBitmask, - ODataJsonReaderUtils.ErrorPropertyBitMask.Message)) - { - return (false, error); + break; } - (isReadSuccessfully, propertyValue) = await this.TryReadErrorStringPropertyValueAsync() - .ConfigureAwait(false); - if (!isReadSuccessfully) + case JsonConstants.ODataErrorMessageName: { - return (false, error); - } + (bool isReadSuccessfully, string propertyValue) = await this.ValidateAndTryReadErrorStringPropertyValueAsync( + ODataJsonReaderUtils.ErrorPropertyBitMask.Message, + ref propertiesFoundBitmask).ConfigureAwait(false); + if (!isReadSuccessfully) + { + return (false, error); + } - error.Message = propertyValue; - break; + error.Message = propertyValue; - case JsonConstants.ODataErrorTargetName: - if (!ODataJsonReaderUtils.ErrorPropertyNotFound( - ref propertiesFoundBitmask, - ODataJsonReaderUtils.ErrorPropertyBitMask.Target)) - { - return (false, error); + break; } - (isReadSuccessfully, propertyValue) = await this.TryReadErrorStringPropertyValueAsync() - .ConfigureAwait(false); - if (!isReadSuccessfully) + case JsonConstants.ODataErrorTargetName: { - return (false, error); - } + (bool isReadSuccessfully, string propertyValue) = await this.ValidateAndTryReadErrorStringPropertyValueAsync( + ODataJsonReaderUtils.ErrorPropertyBitMask.Target, + ref propertiesFoundBitmask).ConfigureAwait(false); + if (!isReadSuccessfully) + { + return (false, error); + } - error.Target = propertyValue; - break; + error.Target = propertyValue; + + break; + } case JsonConstants.ODataErrorDetailsName: - if (!ODataJsonReaderUtils.ErrorPropertyNotFound( + { + if (!ODataJsonReaderUtils.ErrorPropertyNotFound( ref propertiesFoundBitmask, ODataJsonReaderUtils.ErrorPropertyBitMask.Details)) - { - return (false, error); - } + { + return (false, error); + } - (isReadSuccessfully, List errorDetails) = await this.TryReadErrorDetailsPropertyValueAsync() - .ConfigureAwait(false); - if (!isReadSuccessfully) - { - return (false, error); - } + (bool isReadSuccessfully, List errorDetails) = await this.TryReadErrorDetailsPropertyValueAsync() + .ConfigureAwait(false); + if (!isReadSuccessfully) + { + return (false, error); + } - error.Details = errorDetails; - break; + error.Details = errorDetails; - case JsonConstants.ODataErrorInnerErrorName: - if (!ODataJsonReaderUtils.ErrorPropertyNotFound( - ref propertiesFoundBitmask, - ODataJsonReaderUtils.ErrorPropertyBitMask.InnerError)) - { - return (false, error); + break; } - (isReadSuccessfully, ODataInnerError innerError) = await this.TryReadInnerErrorPropertyValueAsync( - recursionDepth: 0).ConfigureAwait(false); - if (!isReadSuccessfully) + case JsonConstants.ODataErrorInnerErrorName: { - return (false, error); - } + if (!ODataJsonReaderUtils.ErrorPropertyNotFound( + ref propertiesFoundBitmask, + ODataJsonReaderUtils.ErrorPropertyBitMask.InnerError)) + { + return (false, error); + } - error.InnerError = innerError; - break; + (bool isReadSuccessfully, ODataInnerError innerError) = await this.TryReadInnerErrorPropertyValueAsync( + recursionDepth: 0).ConfigureAwait(false); + if (!isReadSuccessfully) + { + return (false, error); + } + + error.InnerError = innerError; + + break; + } default: // If we find a non-supported property we don't treat this as an error @@ -1555,7 +1883,7 @@ await this.ReadInternalAsync() /// The value of the TResult parameter contains a tuple comprising of: /// 1). A value of true if an collection was read; otherwise false. /// 2). An collection that was read from the reader or null if none could be read. - private async Task<(bool IsReadSuccessfully, List ErrorDetails)> TryReadErrorDetailsPropertyValueAsync() + private async ValueTask<(bool IsReadSuccessfully, List ErrorDetails)> TryReadErrorDetailsPropertyValueAsync() { Debug.Assert( this.currentBufferedNode.NodeType == JsonNodeType.Property, @@ -1607,7 +1935,7 @@ await ReadInternalAsync() /// The value of the TResult parameter contains a tuple comprising of: /// 1). A value of true if an instance was read; otherwise false. /// 2). An instance that was read from the reader or null if none could be read. - private async Task<(bool IsReadSuccessfully, ODataErrorDetail ErrorDetail)> TryReadErrorDetailAsync() + private async ValueTask<(bool IsReadSuccessfully, ODataErrorDetail ErrorDetail)> TryReadErrorDetailAsync() { Debug.Assert( this.currentBufferedNode.NodeType == JsonNodeType.StartObject, @@ -1618,7 +1946,7 @@ await ReadInternalAsync() if (this.currentBufferedNode.NodeType != JsonNodeType.StartObject) { - return (false, (ODataErrorDetail)null); + return (false, null); } // { @@ -1626,8 +1954,6 @@ await ReadInternalAsync() .ConfigureAwait(false); ODataErrorDetail detail = new ODataErrorDetail(); - bool isReadSuccessfully; - string propertyValue; // We expect one of the supported properties for the value (or EndObject) var propertiesFoundBitmask = ODataJsonReaderUtils.ErrorPropertyBitMask.None; @@ -1638,57 +1964,45 @@ await ReadInternalAsync() switch (propertyName) { case JsonConstants.ODataErrorCodeName: - if (!ODataJsonReaderUtils.ErrorPropertyNotFound( - ref propertiesFoundBitmask, - ODataJsonReaderUtils.ErrorPropertyBitMask.Code)) - { - return (false, detail); - } - - (isReadSuccessfully, propertyValue) = await this.TryReadErrorStringPropertyValueAsync() - .ConfigureAwait(false); - if (!isReadSuccessfully) { - return (false, detail); + (bool isReadSuccessfully, string propertyValue) = await this.ValidateAndTryReadErrorStringPropertyValueAsync( + ODataJsonReaderUtils.ErrorPropertyBitMask.Code, + ref propertiesFoundBitmask).ConfigureAwait(false); + if (!isReadSuccessfully) + { + return (false, detail); + } + + detail.Code = propertyValue; } - - detail.Code = propertyValue; break; case JsonConstants.ODataErrorTargetName: - if (!ODataJsonReaderUtils.ErrorPropertyNotFound( - ref propertiesFoundBitmask, - ODataJsonReaderUtils.ErrorPropertyBitMask.Target)) - { - return (false, detail); - } - - (isReadSuccessfully, propertyValue) = await this.TryReadErrorStringPropertyValueAsync() - .ConfigureAwait(false); - if (!isReadSuccessfully) { - return (false, detail); + (bool isReadSuccessfully, string propertyValue) = await this.ValidateAndTryReadErrorStringPropertyValueAsync( + ODataJsonReaderUtils.ErrorPropertyBitMask.Target, + ref propertiesFoundBitmask).ConfigureAwait(false); + if (!isReadSuccessfully) + { + return (false, detail); + } + + detail.Target = propertyValue; } - - detail.Target = propertyValue; break; case JsonConstants.ODataErrorMessageName: - if (!ODataJsonReaderUtils.ErrorPropertyNotFound( - ref propertiesFoundBitmask, - ODataJsonReaderUtils.ErrorPropertyBitMask.MessageValue)) - { - return (false, detail); - } - - (isReadSuccessfully, propertyValue) = await this.TryReadErrorStringPropertyValueAsync() - .ConfigureAwait(false); - if (!isReadSuccessfully) { - return (false, detail); + (bool isReadSuccessfully, string propertyValue) = await this.ValidateAndTryReadErrorStringPropertyValueAsync( + ODataJsonReaderUtils.ErrorPropertyBitMask.MessageValue, + ref propertiesFoundBitmask).ConfigureAwait(false); + if (!isReadSuccessfully) + { + return (false, detail); + } + + detail.Message = propertyValue; } - - detail.Message = propertyValue; break; default: @@ -1717,7 +2031,7 @@ await this.ReadInternalAsync() /// The value of the TResult parameter contains a tuple comprising of: /// 1). A value of true if an instance was read; otherwise false. /// 2). An instance that was read from the reader or null if none could be read. - private async Task<(bool IsReadSuccessfully, ODataInnerError InnerError)> TryReadInnerErrorPropertyValueAsync(int recursionDepth) + private async ValueTask<(bool IsReadSuccessfully, ODataInnerError InnerError)> TryReadInnerErrorPropertyValueAsync(int recursionDepth) { Debug.Assert(this.currentBufferedNode.NodeType == JsonNodeType.Property, "this.currentBufferedNode.NodeType == JsonNodeType.Property"); Debug.Assert(this.parsingInStreamError, "this.parsingInStreamError"); @@ -1748,88 +2062,55 @@ await this.ReadInternalAsync() { // NOTE: The JSON reader already ensures that the value of a property node is a string string propertyName = (string)this.currentBufferedNode.Value; - string propertyValue; - bool isReadSuccessfully; switch (propertyName) { case JsonConstants.ODataErrorInnerErrorMessageName: - if (!ODataJsonReaderUtils.ErrorPropertyNotFound( - ref propertiesFoundBitmask, - ODataJsonReaderUtils.ErrorPropertyBitMask.MessageValue)) - { - return (false, innerError); - } - - (isReadSuccessfully, propertyValue) = await this.TryReadErrorStringPropertyValueAsync() - .ConfigureAwait(false); - if (!isReadSuccessfully) + if (!await ValidateAndTryReadInnerErrorStringPropertyValueAsync( + innerError, + JsonConstants.ODataErrorInnerErrorMessageName, + ODataJsonReaderUtils.ErrorPropertyBitMask.MessageValue, + ref propertiesFoundBitmask).ConfigureAwait(false)) { return (false, innerError); } - - innerError.Properties.Add( - JsonConstants.ODataErrorInnerErrorMessageName, - new ODataPrimitiveValue(propertyValue)); + break; case JsonConstants.ODataErrorInnerErrorTypeNameName: - if (!ODataJsonReaderUtils.ErrorPropertyNotFound( - ref propertiesFoundBitmask, - ODataJsonReaderUtils.ErrorPropertyBitMask.TypeName)) - { - return (false, innerError); - } - - (isReadSuccessfully, propertyValue) = await this.TryReadErrorStringPropertyValueAsync() - .ConfigureAwait(false); - if (!isReadSuccessfully) + if (!await ValidateAndTryReadInnerErrorStringPropertyValueAsync( + innerError, + JsonConstants.ODataErrorInnerErrorTypeNameName, + ODataJsonReaderUtils.ErrorPropertyBitMask.TypeName, + ref propertiesFoundBitmask).ConfigureAwait(false)) { return (false, innerError); } - innerError.Properties.Add( - JsonConstants.ODataErrorInnerErrorTypeNameName, - new ODataPrimitiveValue(propertyValue)); break; case JsonConstants.ODataErrorInnerErrorStackTraceName: - if (!ODataJsonReaderUtils.ErrorPropertyNotFound( - ref propertiesFoundBitmask, - ODataJsonReaderUtils.ErrorPropertyBitMask.StackTrace)) - { - return (false, innerError); - } - - (isReadSuccessfully, propertyValue) = await this.TryReadErrorStringPropertyValueAsync() - .ConfigureAwait(false); - if (!isReadSuccessfully) + if (!await ValidateAndTryReadInnerErrorStringPropertyValueAsync( + innerError, + JsonConstants.ODataErrorInnerErrorStackTraceName, + ODataJsonReaderUtils.ErrorPropertyBitMask.StackTrace, + ref propertiesFoundBitmask).ConfigureAwait(false)) { return (false, innerError); } - innerError.Properties.Add( - JsonConstants.ODataErrorInnerErrorStackTraceName, - new ODataPrimitiveValue(propertyValue)); break; case JsonConstants.ODataErrorInnerErrorInnerErrorName: case JsonConstants.ODataErrorInnerErrorName: - if (!ODataJsonReaderUtils.ErrorPropertyNotFound( + if (!await ValidateAndTryReadNestedInnerErrorAsync( + innerError, ref propertiesFoundBitmask, - ODataJsonReaderUtils.ErrorPropertyBitMask.InnerError)) + recursionDepth).ConfigureAwait(false)) { return (false, innerError); } - (isReadSuccessfully, ODataInnerError nestedInnerError) = await TryReadInnerErrorPropertyValueAsync(recursionDepth) - .ConfigureAwait(false); - if (!isReadSuccessfully) - { - return (false, innerError); - } - - innerError.InnerError = nestedInnerError; break; default: @@ -1850,6 +2131,136 @@ await this.ReadInternalAsync() return (true, innerError); } + /// + /// Validates the top-level error string property is not a duplicate, then attempts to read its string (or null) value. + /// + /// Bit representing this property. + /// Bitmask tracking previously seen properties (updated if successful). + /// + /// (IsReadSuccessfully, propertyValue) where IsReadSuccessfully is false on duplicate or non-string/non-null value. + /// + private ValueTask<(bool IsReadSuccessfully, string propertyValue)> ValidateAndTryReadErrorStringPropertyValueAsync( + ODataJsonReaderUtils.ErrorPropertyBitMask propertyBitMask, + ref ODataJsonReaderUtils.ErrorPropertyBitMask propertiesFoundBitmask) + { + // Duplicate or already seen property? + if (!ODataJsonReaderUtils.ErrorPropertyNotFound(ref propertiesFoundBitmask, propertyBitMask)) + { + return ValueTask.FromResult((false, (string)null)); + } + + // TryReadErrorStringPropertyValueAsync has fast path for completed sync reads + return this.TryReadErrorStringPropertyValueAsync(); + } + + /// + /// Validates the inner error string property is not a duplicate, then tries to read it and add it to . + /// + /// Target inner error being populated. + /// Canonical JSON property name (message/type/stacktrace). + /// Bit representing this property. + /// Bitmask tracking previously seen properties (updated if successful). + /// true if the property was successfully read and added; otherwise false. + private ValueTask ValidateAndTryReadInnerErrorStringPropertyValueAsync( + ODataInnerError innerError, + string propertyName, + ODataJsonReaderUtils.ErrorPropertyBitMask propertyBitMask, + ref ODataJsonReaderUtils.ErrorPropertyBitMask propertiesFoundBitmask) + { + // Duplicate or already seen property? + if (!ODataJsonReaderUtils.ErrorPropertyNotFound(ref propertiesFoundBitmask, propertyBitMask)) + { + return ValueTask.FromResult(false); + } + + ValueTask<(bool IsReadSuccessfully, string PropertyValue)> readPropertyValueTask = this.TryReadErrorStringPropertyValueAsync(); + if (readPropertyValueTask.IsCompletedSuccessfully) + { + return ValueTask.FromResult(ProcessResult(innerError, propertyName, readPropertyValueTask.Result)); + } + + return AwaitTryReadAndSetInnerErrorStringPropertyValueAsync(this, innerError, propertyName, readPropertyValueTask); + + static bool ProcessResult( + ODataInnerError targetInnerError, + string targetPropertyName, + (bool IsReadSuccessfully, string PropertyValue) result) + { + if (!result.IsReadSuccessfully) + { + return false; + } + + targetInnerError.Properties.Add(targetPropertyName, new ODataPrimitiveValue(result.PropertyValue)); + + return true; + } + + static async ValueTask AwaitTryReadAndSetInnerErrorStringPropertyValueAsync( + BufferingJsonReader thisParam, + ODataInnerError innerError, + string propertyName, + ValueTask<(bool IsReadSuccessfully, string PropertyValue)> pendingReadPropertyValueTask) + { + (bool IsReadSuccessfully, string PropertyValue) readPropertyValueTask = await pendingReadPropertyValueTask.ConfigureAwait(false); + + return ProcessResult(innerError, propertyName, readPropertyValueTask); + } + } + + /// + /// Validates the nested inner error property is not a duplicate, then recursively reads and assigns the nested inner error. + /// + /// Target inner error receiving the nested InnerError. + /// Bitmask tracking previously seen properties (updated if successful). + /// Current recursion depth (used for depth validation). + /// true if a nested inner error was successfully read and attached; otherwise false. + private ValueTask ValidateAndTryReadNestedInnerErrorAsync( + ODataInnerError innerError, + ref ODataJsonReaderUtils.ErrorPropertyBitMask propertiesFoundBitmask, + int recursionDepth) + { + // Duplicate or already seen property? + if (!ODataJsonReaderUtils.ErrorPropertyNotFound( + ref propertiesFoundBitmask, + ODataJsonReaderUtils.ErrorPropertyBitMask.InnerError)) + { + return ValueTask.FromResult(false); + } + + ValueTask<(bool IsReadSuccessfully, ODataInnerError InnerError)> readPropertyValueTask = this.TryReadInnerErrorPropertyValueAsync(recursionDepth); + if (readPropertyValueTask.IsCompletedSuccessfully) + { + return ValueTask.FromResult(ProcessResult(innerError, readPropertyValueTask.Result)); + } + + return AwaitTryReadInnerErrorPropertyValueAsync(this, innerError, readPropertyValueTask); + + static bool ProcessResult( + ODataInnerError targetInnerError, + (bool IsReadSuccessfully, ODataInnerError InnerError) result) + { + if (!result.IsReadSuccessfully) + { + return false; + } + + targetInnerError.InnerError = result.InnerError; + + return true; + } + + static async ValueTask AwaitTryReadInnerErrorPropertyValueAsync( + BufferingJsonReader thisParam, + ODataInnerError innerError, + ValueTask<(bool IsReadSuccessfully, ODataInnerError InnerError)> pendingReadPropertyValueTask) + { + (bool IsReadSuccessfully, ODataInnerError InnerError) readPropertyValueTask = await pendingReadPropertyValueTask.ConfigureAwait(false); + + return ProcessResult(innerError, readPropertyValueTask); + } + } + /// /// Asynchronously try to read the string value of a property. /// @@ -1857,21 +2268,36 @@ await this.ReadInternalAsync() /// The value of the TResult parameter contains a tuple comprising of: /// 1). A value of true if a string value (or null) was read as property value of the current property; otherwise false. /// 2). The string value read if the method returns true; otherwise null. - private async Task<(bool IsReadSuccessfully, string PropertyValue)> TryReadErrorStringPropertyValueAsync() + private ValueTask<(bool IsReadSuccessfully, string PropertyValue)> TryReadErrorStringPropertyValueAsync() { - Debug.Assert(this.currentBufferedNode.NodeType == JsonNodeType.Property, "this.currentBufferedNode.NodeType == JsonNodeType.Property"); + Debug.Assert(this.currentBufferedNode.NodeType == JsonNodeType.Property, + "this.currentBufferedNode.NodeType == JsonNodeType.Property"); Debug.Assert(this.parsingInStreamError, "this.parsingInStreamError"); this.AssertBuffering(); this.AssertAsynchronous(); - await this.ReadInternalAsync() - .ConfigureAwait(false); + Task readInternalTask = this.ReadInternalAsync(); + if (readInternalTask.IsCompletedSuccessfully) + { + string stringValue = this.currentBufferedNode.Value as string; + bool isReadSuccessfully = this.currentBufferedNode.NodeType == JsonNodeType.PrimitiveValue + && (this.currentBufferedNode.Value == null || stringValue != null); + + return ValueTask.FromResult((isReadSuccessfully, stringValue)); + } - // We expect a string value - string stringValue = this.currentBufferedNode.Value as string; - return ( - this.currentBufferedNode.NodeType == JsonNodeType.PrimitiveValue && (this.currentBufferedNode.Value == null || stringValue != null), - stringValue); + return AwaitReadInternalAsync(this, readInternalTask); + + static async ValueTask<(bool, string)> AwaitReadInternalAsync(BufferingJsonReader thisParam, Task pendingReadInternalTask) + { + await pendingReadInternalTask.ConfigureAwait(false); + + string stringValue = thisParam.currentBufferedNode.Value as string; + bool isReadSuccessfully = thisParam.currentBufferedNode.NodeType == JsonNodeType.PrimitiveValue + && (thisParam.currentBufferedNode.Value == null || stringValue != null); + + return (isReadSuccessfully, stringValue); + } } /// @@ -1884,36 +2310,45 @@ await this.ReadInternalAsync() /// Post-Condition: JsonNodeType.PrimitiveValue, JsonNodeType.EndArray or JsonNodeType.EndObject /// /// A task that represents the asynchronous read operation. - private async Task SkipValueInternalAsync() + private ValueTask SkipValueInternalAsync() { this.AssertAsynchronous(); int depth = 0; - do + + while (true) { - switch (this.currentBufferedNode.NodeType) + depth = AdjustDepth(depth, this.currentBufferedNode.NodeType); + + Task readInternalTask = this.ReadInternalAsync(); + if (!readInternalTask.IsCompletedSuccessfully) { - case JsonNodeType.PrimitiveValue: - break; - 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( - this.currentBufferedNode.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; + return AwaitSkipValueInternalAsync(this, depth, readInternalTask); } - await this.ReadInternalAsync() - .ConfigureAwait(false); - } while (depth > 0); + // After completed synchronous advance, if depth is 0, we're done + if (depth == 0) + { + return ValueTask.CompletedTask; + } + } + + static async ValueTask AwaitSkipValueInternalAsync(BufferingJsonReader thisParam, int depth, Task pendingReadInternalTask) + { + while (true) + { + await pendingReadInternalTask.ConfigureAwait(false); + + if (depth == 0) + { + return; + } + + depth = AdjustDepth(depth, thisParam.currentBufferedNode.NodeType); + + pendingReadInternalTask = thisParam.ReadInternalAsync(); + } + } } /// @@ -1923,7 +2358,7 @@ await this.ReadInternalAsync() /// The node value. private void AddNewNodeToTheEndOfBufferedNodesList(JsonNodeType nodeType, object value) { - BufferedNode newNode = new BufferedNode(nodeType, value); + BufferedNode newNode = RentNode(nodeType, value); newNode.Previous = this.bufferedNodesHead.Previous; newNode.Next = this.bufferedNodesHead; this.bufferedNodesHead.Previous.Next = newNode; @@ -1968,16 +2403,92 @@ private bool NodeExistsInCurrentBuffer(BufferedNode nodeToCheck) } #endif + + /// + /// Adjusts the running nesting depth counter based on the current node type, + /// incrementing for start scopes and decrementing for end scopes. + /// + /// The current nesting depth. + /// The node just encountered. + /// The updated depth. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int AdjustDepth(int currentDepth, JsonNodeType nodeType) + { + switch (nodeType) + { + case JsonNodeType.PrimitiveValue: + return currentDepth; + + case JsonNodeType.StartArray: + case JsonNodeType.StartObject: + return currentDepth + 1; + + case JsonNodeType.EndArray: + case JsonNodeType.EndObject: + Debug.Assert(currentDepth > 0, "Seen too many scope ends."); + return currentDepth - 1; + + default: + Debug.Assert( + 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."); + return currentDepth; + } + } + + /// + /// Rents a node from the per-thread pool or creates a new instance. + /// + /// The node type. + /// The node value. + /// A rented or newly created node. + private static BufferedNode RentNode(JsonNodeType nodeType, object nodeValue) + { + Stack nodePool = bufferedNodePool; + if (nodePool != null && nodePool.Count > 0) + { + BufferedNode node = nodePool.Pop(); + node.Reset(nodeType, nodeValue); + + return node; + } + + return new BufferedNode(nodeType, nodeValue); + } + + /// + /// Returns a node to the per-thread pool (breaking links and clearing value to release references). + /// + /// The node to return. + private static void ReturnNode(BufferedNode node) + { + if (node == null) + { + return; + } + + // Break object graph and value references + node.Reset(JsonNodeType.None, null); + + Stack nodePool = bufferedNodePool ?? new Stack(); + if (nodePool.Count < MaxPooledNodesPerThread) + { + nodePool.Push(node); + bufferedNodePool = nodePool; + } + // else let it be GC'ed + } + /// /// Private class used to buffer nodes when reading in buffering mode. /// protected internal sealed class BufferedNode { /// The type of the node read. - private readonly JsonNodeType nodeType; + private JsonNodeType nodeType; /// The value of the node. - private readonly object nodeValue; + private object nodeValue; /// /// Constructor. @@ -1992,6 +2503,20 @@ internal BufferedNode(JsonNodeType nodeType, object value) this.Next = this; } + /// + /// Reinitializes the node for reuse. + /// + /// The type of the node. + /// The value of the node. + internal void Reset(JsonNodeType nodeType, object nodeValue) + { + this.nodeType = nodeType; + this.nodeValue = nodeValue; + // Break any previous links + this.Previous = this; + this.Next = this; + } + /// /// The type of the node read. /// diff --git a/src/Microsoft.OData.Core/Json/JsonReader.cs b/src/Microsoft.OData.Core/Json/JsonReader.cs index 4b9342c437..55fe118c40 100644 --- a/src/Microsoft.OData.Core/Json/JsonReader.cs +++ b/src/Microsoft.OData.Core/Json/JsonReader.cs @@ -612,177 +612,104 @@ public Task CanStreamAsync() /// A task that represents the asynchronous read operation. /// The value of the TResult parameter contains true if a new node was found, /// or false if end of input was reached. - [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Not really feasible to extract code to methods without introducing unnecessary complexity.")] - public virtual async Task ReadAsync() + public virtual Task ReadAsync() { if (this.readingStream) { - throw JsonReaderExtensions.CreateException(SRResources.JsonReader_CannotCallReadInStreamState); + return Task.FromException( + JsonReaderExtensions.CreateException(SRResources.JsonReader_CannotCallReadInStreamState)); } - if (this.canStream) + // Consume a pending (unmaterialized) primitive if present + ValueTask consumePendingPrimitiveTask = this.ConsumePendingPrimitiveAsync(); + if (!consumePendingPrimitiveTask.IsCompletedSuccessfully) { - this.canStream = false; - if (this.nodeType == JsonNodeType.PrimitiveValue) - { - // caller is positioned on a string value that they haven't read, so skip it - if (this.characterBuffer[this.tokenStartIndex] == 'n') - { - await this.ParseNullPrimitiveValueAsync().ConfigureAwait(false); - } - else - { - await this.ParseStringPrimitiveValueAsync().ConfigureAwait(false); - } - } + return AwaitConsumePendingPrimitiveAsync(this, consumePendingPrimitiveTask); } - // 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 + return ContinueAfterConsumePendingPrimitiveAsync(this); - // Skip any whitespace characters. - // This also makes sure that we have at least one non-whitespace character available. - if (!await this.SkipWhitespacesAsync().ConfigureAwait(false)) + static Task ContinueAfterConsumePendingPrimitiveAsync(JsonReader thisParam) { - return this.EndOfInput(); - } - - Debug.Assert( - this.tokenStartIndex < this.storedCharacterCount && !IsWhitespaceCharacter(this.characterBuffer[this.tokenStartIndex]), - "The SkipWhitespacesAsync didn't correctly skip all whitespace characters from the input."); + // Reset the node value. + thisParam.nodeValue = null; - Scope currentScope = this.scopes.Peek(); - - bool commaFound = false; - if (this.characterBuffer[this.tokenStartIndex] == ',') - { - commaFound = true; - this.tokenStartIndex++; +#if DEBUG + // Reset the node type to None - so that we can verify that the ReadAsync method actually sets it. + thisParam.nodeType = JsonNodeType.None; +#endif - // 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 (!await this.SkipWhitespacesAsync().ConfigureAwait(false)) + ValueTask<(bool CommaFound, bool EndOfInput)> prepareForNextTokenTask = thisParam.PrepareForNextTokenAsync(); + if (!prepareForNextTokenTask.IsCompletedSuccessfully) { - return this.EndOfInput(); + return AwaitPrepareForNextTokenAsync(thisParam, prepareForNextTokenTask); } - Debug.Assert( - this.tokenStartIndex < this.storedCharacterCount && !IsWhitespaceCharacter(this.characterBuffer[this.tokenStartIndex]), - "The SkipWhitespacesAsync didn't correctly skip all whitespace characters from the input."); + return ContinueAfterPrepareForNextTokenAsync(thisParam, prepareForNextTokenTask.Result); } - switch (currentScope.Type) + static Task ContinueAfterPrepareForNextTokenAsync( + JsonReader thisParam, + (bool CommaFound, bool EndOfInput) prepareForNextTokenResult) { - case ScopeType.Root: - if (commaFound) - { - throw JsonReaderExtensions.CreateException(Error.Format(SRResources.JsonReader_UnexpectedComma, ScopeType.Root)); - } - - if (currentScope.ValueCount > 0) - { - // We already found the top-level value, so fail - throw JsonReaderExtensions.CreateException(SRResources.JsonReader_MultipleTopLevelValues); - } - - // We expect a "value" - start array, start object or primitive value - this.nodeType = await this.ParseValueAsync() - .ConfigureAwait(false); - break; - - case ScopeType.Array: - if (commaFound && currentScope.ValueCount == 0) - { - throw JsonReaderExtensions.CreateException(Error.Format(SRResources.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(Error.Format(SRResources.JsonReader_UnexpectedComma, ScopeType.Array)); - } - - this.PopScope(); - this.nodeType = JsonNodeType.EndArray; - break; - } + (bool commaFound, bool endOfInput) = prepareForNextTokenResult; - if (!commaFound && currentScope.ValueCount > 0) - { - throw JsonReaderExtensions.CreateException(Error.Format(SRResources.JsonReader_MissingComma, ScopeType.Array)); - } + if (endOfInput) + { + // EndOfInput always returns false + bool innerResult = thisParam.EndOfInput(); + Debug.Assert(innerResult == false, "EndOfInput should always return false."); + + return CachedTasks.False; + } - // We expect element which is a "value" - start array, start object or primitive value - this.nodeType = await this.ParseValueAsync() - .ConfigureAwait(false); - break; + ValueTask readInScopeTask = thisParam.ReadInScopeAsync(commaFound); + if (!readInScopeTask.IsCompletedSuccessfully) + { + return AwaitReadInScopeAsync(thisParam, readInScopeTask); + } - case ScopeType.Object: - if (commaFound && currentScope.ValueCount == 0) - { - throw JsonReaderExtensions.CreateException(Error.Format(SRResources.JsonReader_UnexpectedComma, ScopeType.Object)); - } + thisParam.nodeType = readInScopeTask.Result; - // We might see end of object here - if (this.characterBuffer[this.tokenStartIndex] == '}') - { - this.tokenStartIndex++; + Debug.Assert( + thisParam.nodeType != JsonNodeType.None && thisParam.nodeType != JsonNodeType.EndOfInput, + "Read should never go back to None and EndOfInput should be reported by directly returning."); + + return CachedTasks.True; + } - // End of object is only valid when there was no comma before it. - if (commaFound) - { - throw JsonReaderExtensions.CreateException(Error.Format(SRResources.JsonReader_UnexpectedComma, ScopeType.Object)); - } + static async Task AwaitConsumePendingPrimitiveAsync( + JsonReader thisParam, + ValueTask pendingConsumePendingPrimitiveTask) + { + await pendingConsumePendingPrimitiveTask.ConfigureAwait(false); - this.PopScope(); - this.nodeType = JsonNodeType.EndObject; - break; - } - else - { - if (!commaFound && currentScope.ValueCount > 0) - { - throw JsonReaderExtensions.CreateException(Error.Format(SRResources.JsonReader_MissingComma, ScopeType.Object)); - } + return await ContinueAfterConsumePendingPrimitiveAsync(thisParam).ConfigureAwait(false); + } - // We expect a property here - this.nodeType = await this.ParsePropertyAsync() - .ConfigureAwait(false); - break; - } + static async Task AwaitPrepareForNextTokenAsync( + JsonReader thisParam, + ValueTask<(bool CommaFound, bool EndOfInput)> pendingPrepareForNextTokenTask) + { + (bool commaFound, bool endOfInput) = await pendingPrepareForNextTokenTask.ConfigureAwait(false); + + return await ContinueAfterPrepareForNextTokenAsync( + thisParam, + (commaFound, endOfInput)).ConfigureAwait(false); + } - case ScopeType.Property: - if (commaFound) - { - throw JsonReaderExtensions.CreateException(Error.Format(SRResources.JsonReader_UnexpectedComma, ScopeType.Property)); - } + static async Task AwaitReadInScopeAsync( + JsonReader thisParam, + ValueTask pendingReadInScopeTask) + { + thisParam.nodeType = await pendingReadInScopeTask.ConfigureAwait(false); - // We expect the property value, which is a "value" - start array, start object or primitive value - this.nodeType = await this.ParseValueAsync() - .ConfigureAwait(false); - break; + Debug.Assert( + thisParam.nodeType != JsonNodeType.None && thisParam.nodeType != JsonNodeType.EndOfInput, + "Read should never go back to None and EndOfInput should be reported by directly returning."); - default: - throw JsonReaderExtensions.CreateException(Error.Format(SRResources.General_InternalError, InternalErrorCodes.JsonReader_Read)); + return true; } - - 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; } /// @@ -869,102 +796,453 @@ public ValueTask DisposeAsync() } /// - /// Determines if a given character is a whitespace character. + /// Consumes (skips) a deferred primitive value (string or null) if the reader is positioned + /// on an unmaterialized streamable primitive. No-op if streaming is not possible or the + /// current node is not a primitive value. /// - /// The character to test. - /// true if the is a whitespace; false otherwise. - /// Note that the behavior of this method is different from Char.IsWhitespace, since that method - /// returns true for all characters defined as whitespace by the Unicode spec (which is a lot of characters), - /// this one on the other hand recognizes just the whitespaces as defined by the JSON spec. - [DebuggerStepThrough] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsWhitespaceCharacter(char character) + /// + /// A completed ValueTask if the skip finished synchronously; otherwise a ValueTask that + /// completes when the primitive has been fully skipped. + /// + private ValueTask ConsumePendingPrimitiveAsync() { - // The whitespace characters are 0x20 (space), 0x09 (tab), 0x0A (new line), 0x0D (carriage return) - // Anything above 0x20 is a non-whitespace character. - if (character > (char)0x20 || character != (char)0x20 && character != (char)0x09 && character != (char)0x0A && character != (char)0x0D) + if (!this.canStream) { - return false; + return ValueTask.CompletedTask; } - else + + this.canStream = false; + + if (this.nodeType != JsonNodeType.PrimitiveValue) { - return true; + return ValueTask.CompletedTask; + } + + // caller is positioned on a string value that they haven't read, so skip it + if (this.characterBuffer[this.tokenStartIndex] == 'n') + { + ValueTask parseNullTask = this.ParseNullPrimitiveValueAsync(); + if (parseNullTask.IsCompletedSuccessfully) + { + return ValueTask.CompletedTask; + } + + return AwaitParseNullAsync(parseNullTask); + } + + ValueTask<(string, bool)> parseStringTask = this.ParseStringPrimitiveValueAsync(); + if (parseStringTask.IsCompletedSuccessfully) + { + return ValueTask.CompletedTask; + } + + return AwaitParseStringAsync(parseStringTask); + + static async ValueTask AwaitParseNullAsync(ValueTask pendingParseNullTask) + { + await pendingParseNullTask.ConfigureAwait(false); + } + + static async ValueTask AwaitParseStringAsync(ValueTask<(string, bool)> pendingParseStringTask) + { + await pendingParseStringTask.ConfigureAwait(false); } } /// - /// Parses a "value", that is an array, object or primitive value. + /// Skips leading whitespace and an optional comma plus following whitespace in the input. /// - /// The node type to report to the user. - private JsonNodeType ParseValue() + /// + /// A ValueTask whose result tuple indicates whether a comma was found and whether end of input + /// was reached. + /// + private ValueTask<(bool CommaFound, bool EndOfInput)> PrepareForNextTokenAsync() { - 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 occur at the root, in array or as a property value."); - - // Increase the count of values under the current scope. - this.scopes.Peek().ValueCount++; + // Skip any whitespace characters. + // This also makes sure that we have at least one non-whitespace character available. + ValueTask leadingWhitespaceTask = this.SkipWhitespacesAsync(); + if (!leadingWhitespaceTask.IsCompletedSuccessfully) + { + return AwaitLeadingWhitespaceAsync(this, leadingWhitespaceTask); + } - char currentCharacter = this.characterBuffer[this.tokenStartIndex]; - switch (currentCharacter) + if (!leadingWhitespaceTask.Result) { - case '{': - // Start of object - this.PushScope(ScopeType.Object); - this.tokenStartIndex++; - return JsonNodeType.StartObject; + // All remaining input was whitespace -> EOF + return ValueTask.FromResult((false, true)); + } - case '[': - // Start of array - this.PushScope(ScopeType.Array); - this.tokenStartIndex++; - this.SkipWhitespaces(); - this.canStream = - this.characterBuffer[this.tokenStartIndex] == '"' || - this.characterBuffer[this.tokenStartIndex] == '\'' || - this.characterBuffer[this.tokenStartIndex] == 'n'; - return JsonNodeType.StartArray; + return ContinueAfterLeadingWhitespaceAsync(this); - case '"': - case '\'': - // String primitive value - // Don't parse yet, as it may be a stream. Defer parsing until .Value is called. - this.canStream = true; - break; + static ValueTask<(bool CommaFound, bool EndOfInput)> ContinueAfterLeadingWhitespaceAsync(JsonReader thisParam) + { + Debug.Assert( + thisParam.tokenStartIndex < thisParam.storedCharacterCount && !IsWhitespaceCharacter(thisParam.characterBuffer[thisParam.tokenStartIndex]), + "The SkipWhitespacesAsync didn't correctly skip all whitespace characters from the input."); - case 'n': - // Null value - // Don't parse yet, as user may be streaming a stream. Defer parsing until .Value is called. - this.canStream = true; - break; + bool commaFound = false; - case 't': - case 'f': - this.nodeValue = this.ParseBooleanPrimitiveValue(); - break; + if (thisParam.characterBuffer[thisParam.tokenStartIndex] == ',') + { + commaFound = true; + thisParam.tokenStartIndex++; - 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 == '.')) + // NOTE: The validity of the comma is verified later depending on the current scope. + // Skip all whitespaces after comma. + ValueTask postCommaWhitespaceTask = thisParam.SkipWhitespacesAsync(); + if (!postCommaWhitespaceTask.IsCompletedSuccessfully) { - this.nodeValue = this.ParseNumberPrimitiveValue(); - break; + return AwaitPostCommaWhitespaceAsync(thisParam, commaFound, postCommaWhitespaceTask); } - else + + if (!postCommaWhitespaceTask.Result) { - // Unknown token - fail. - throw JsonReaderExtensions.CreateException(SRResources.JsonReader_UnrecognizedToken); + // Comma followed only by trailing whitespace -> EOF + return ValueTask.FromResult((true, true)); } - } - this.TryPopPropertyScope(); - return JsonNodeType.PrimitiveValue; - } + Debug.Assert( + thisParam.tokenStartIndex < thisParam.storedCharacterCount && !IsWhitespaceCharacter(thisParam.characterBuffer[thisParam.tokenStartIndex]), + "The SkipWhitespacesAsync didn't correctly skip all whitespace characters from the input."); + } - /// - /// Parses a property name and the colon after it. + return ValueTask.FromResult((commaFound, false)); + } + + static async ValueTask<(bool CommaFound, bool EndOfInput)> AwaitLeadingWhitespaceAsync( + JsonReader thisParam, + ValueTask pendingLeadingWhitespaceTask) + { + if (!await GetOrAwait(pendingLeadingWhitespaceTask).ConfigureAwait(false)) + { + return (false, true); + } + + return await ContinueAfterLeadingWhitespaceAsync(thisParam).ConfigureAwait(false); + } + + static async ValueTask<(bool CommaFound, bool EndOfInput)> AwaitPostCommaWhitespaceAsync( + JsonReader thisParam, + bool commaFound, + ValueTask pendingPostCommaWhitespaceTask) + { + if (!await GetOrAwait(pendingPostCommaWhitespaceTask).ConfigureAwait(false)) + { + // Comma followed only by trailing whitespace -> EOF + return (commaFound, true); + } + + Debug.Assert( + thisParam.tokenStartIndex < thisParam.storedCharacterCount && !IsWhitespaceCharacter(thisParam.characterBuffer[thisParam.tokenStartIndex]), + "The SkipWhitespacesAsync didn't correctly skip all whitespace characters from the input."); + + return (commaFound, false); + } + } + + /// + /// Dispatches reading logic for the current scope type, validating comma usage and returning the next node type. + /// + /// true if a comma was detected before this scope’s next token. + /// A ValueTask producing the next ; otherwise faulted task. + private ValueTask ReadInScopeAsync(bool commaFound) + { + Scope currentScope = this.scopes.Peek(); + + switch (currentScope.Type) + { + case ScopeType.Root: + return this.ReadInRootScopeAsync(commaFound, currentScope); + + case ScopeType.Array: + return this.ReadInArrayScopeAsync(commaFound, currentScope); + + case ScopeType.Object: + return this.ReadInObjectScopeAsync(commaFound, currentScope); + + case ScopeType.Property: + return this.ReadInPropertyScopeAsync(commaFound); + + default: + return ValueTask.FromException( + JsonReaderExtensions.CreateException( + Error.Format(SRResources.General_InternalError, InternalErrorCodes.JsonReader_Read))); + } + } + + /// + /// Processes the next token while in the root scope. + /// Validates that only a single top-level value appears and no leading comma exists. + /// + /// true if a comma preceded this position - invalid for root scope. + /// The current root scope instance. + /// A ValueTask producing the next ; otherwise faulted task. + private ValueTask ReadInRootScopeAsync(bool commaFound, Scope currentScope) + { + if (commaFound) + { + return ValueTask.FromException( + JsonReaderExtensions.CreateException( + Error.Format(SRResources.JsonReader_UnexpectedComma, ScopeType.Root))); + } + + if (currentScope.ValueCount > 0) + { + // We already found the top-level value, so fail + return ValueTask.FromException( + JsonReaderExtensions.CreateException(SRResources.JsonReader_MultipleTopLevelValues)); + } + + // We expect a "value" - start array, start object or primitive value + ValueTask parseValueTask = this.ParseValueAsync(); + if (parseValueTask.IsCompletedSuccessfully) + { + return ValueTask.FromResult(parseValueTask.Result); + } + + return AwaitParseValueAsync(parseValueTask); + + static async ValueTask AwaitParseValueAsync(ValueTask pendingParseValueTask) + { + return await pendingParseValueTask.ConfigureAwait(false); + } + } + + /// + /// Processes the next token while in an array scope. + /// Handles end-of-array, comma rules, and element parsing. + /// + /// true if a comma preceded this position. + /// The active array scope. + /// A ValueTask producing the next ; otherwise faulted task. + private ValueTask ReadInArrayScopeAsync(bool commaFound, Scope currentScope) + { + if (commaFound && currentScope.ValueCount == 0) + { + return ValueTask.FromException( + JsonReaderExtensions.CreateException( + Error.Format(SRResources.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) + { + return ValueTask.FromException( + JsonReaderExtensions.CreateException( + Error.Format(SRResources.JsonReader_UnexpectedComma, ScopeType.Array))); + } + + this.PopScope(); + return ValueTask.FromResult(JsonNodeType.EndArray); + } + + if (!commaFound && currentScope.ValueCount > 0) + { + return ValueTask.FromException( + JsonReaderExtensions.CreateException( + Error.Format(SRResources.JsonReader_MissingComma, ScopeType.Array))); + } + + // We expect element which is a "value" - start array, start object or primitive value + ValueTask parseValueTask = this.ParseValueAsync(); + if (parseValueTask.IsCompletedSuccessfully) + { + return ValueTask.FromResult(parseValueTask.Result); + } + + return AwaitParseValueAsync(parseValueTask); + + static async ValueTask AwaitParseValueAsync(ValueTask pendingParseValueTask) + { + return await pendingParseValueTask.ConfigureAwait(false); + } + } + + /// + /// Processes the next token while in an object scope. + /// Handles end-of-object, comma validation, and property parsing. + /// + /// true if a comma preceded this position. + /// The active object scope. + /// A ValueTask producing the next ; otherwise faulted task. + private ValueTask ReadInObjectScopeAsync(bool commaFound, Scope currentScope) + { + if (commaFound && currentScope.ValueCount == 0) + { + return ValueTask.FromException( + JsonReaderExtensions.CreateException( + Error.Format(SRResources.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) + { + return ValueTask.FromException( + JsonReaderExtensions.CreateException( + Error.Format(SRResources.JsonReader_UnexpectedComma, ScopeType.Object))); + } + + this.PopScope(); + return ValueTask.FromResult(JsonNodeType.EndObject); + } + + if (!commaFound && currentScope.ValueCount > 0) + { + return ValueTask.FromException( + JsonReaderExtensions.CreateException( + Error.Format(SRResources.JsonReader_MissingComma, ScopeType.Object))); + } + + // We expect a property here + ValueTask parsePropertyTask = this.ParsePropertyAsync(); + if (parsePropertyTask.IsCompletedSuccessfully) + { + return ValueTask.FromResult(parsePropertyTask.Result); + } + + return AwaitParsePropertyAsync(parsePropertyTask); + + static async ValueTask AwaitParsePropertyAsync(ValueTask pendingParsePropertyTask) + { + return await pendingParsePropertyTask.ConfigureAwait(false); + } + } + + /// + /// Processes the next token while in an property scope; validates that no comma precedes the value. + /// + /// true if a comma preceded this position - invalid in property scope. + /// A ValueTask producing the next ; otherwise faulted task. + private ValueTask ReadInPropertyScopeAsync(bool commaFound) + { + if (commaFound) + { + return ValueTask.FromException(JsonReaderExtensions.CreateException( + Error.Format(SRResources.JsonReader_UnexpectedComma, ScopeType.Property))); + } + + // We expect the property value, which is a "value" - start array, start object or primitive value + ValueTask parseValueTask = this.ParseValueAsync(); + if (parseValueTask.IsCompletedSuccessfully) + { + return ValueTask.FromResult(parseValueTask.Result); + } + + return AwaitParseValueAsync(parseValueTask); + + static async ValueTask AwaitParseValueAsync(ValueTask pendingParseValueTask) + { + return await pendingParseValueTask.ConfigureAwait(false); + } + } + + /// + /// Determines if a given character is a whitespace character. + /// + /// The character to test. + /// true if the is a whitespace; false otherwise. + /// Note that the behavior of this method is different from Char.IsWhitespace, since that method + /// returns true for all characters defined as whitespace by the Unicode spec (which is a lot of characters), + /// this one on the other hand recognizes just the whitespaces as defined by the JSON spec. + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsWhitespaceCharacter(char character) + { + // The whitespace characters are 0x20 (space), 0x09 (tab), 0x0A (new line), 0x0D (carriage return) + // Anything above 0x20 is a non-whitespace character. + if (character > (char)0x20 || character != (char)0x20 && character != (char)0x09 && character != (char)0x0A && character != (char)0x0D) + { + return false; + } + else + { + return true; + } + } + + /// + /// Parses a "value", that is an array, object or primitive value. + /// + /// The node type to report to the user. + 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 occur 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++; + this.SkipWhitespaces(); + this.canStream = + this.characterBuffer[this.tokenStartIndex] == '"' || + this.characterBuffer[this.tokenStartIndex] == '\'' || + this.characterBuffer[this.tokenStartIndex] == 'n'; + return JsonNodeType.StartArray; + + case '"': + case '\'': + // String primitive value + // Don't parse yet, as it may be a stream. Defer parsing until .Value is called. + this.canStream = true; + break; + + case 'n': + // Null value + // Don't parse yet, as user may be streaming a stream. Defer parsing until .Value is called. + this.canStream = true; + 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(SRResources.JsonReader_UnrecognizedToken); + } + } + + this.TryPopPropertyScope(); + return JsonNodeType.PrimitiveValue; + } + + /// + /// Parses a property name and the colon after it. /// /// The node type to report to the user. private JsonNodeType ParseProperty() @@ -1120,7 +1398,7 @@ private string ParseStringPrimitiveValue(out bool hasLeadingBackslash) string unicodeHexValue = this.ConsumeTokenToString(4); int characterValue; - if (!Int32.TryParse(unicodeHexValue, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out characterValue)) + if (!int.TryParse(unicodeHexValue, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out characterValue)) { throw JsonReaderExtensions.CreateException(Error.Format(SRResources.JsonReader_UnrecognizedEscapeSequence, "\\u" + unicodeHexValue)); } @@ -1562,6 +1840,60 @@ private void ConsumeTokenAppendToBuilder(StringBuilder builder, int characterCou this.tokenStartIndex += characterCount; } + /// + /// Flushes pending literal characters into and resets to 0. + /// + /// The instance to append token to. + /// Count of contiguous unflushed literal characters; reset to 0. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void FlushParsedLiteralToBuilder(StringBuilder builder, ref int bufferedLiteralLength) + { + if (bufferedLiteralLength == 0) + { + return; + } + + // Append everything up to parsed length to the builder. + this.ConsumeTokenAppendToBuilder(builder, bufferedLiteralLength); + bufferedLiteralLength = 0; + } + + /// + /// Materializes the current JSON string literal (with optional escape resolution) into a managed string, + /// flushing any remained buffered literal characters, validating and consuming the terminating quote. + /// + /// + /// Optional StringBuilder holding previously flushed segments - null if no escape sequences were resolved. + /// + /// + /// Count of contiguous unflushed literal characters immediately preceding the terminating quote character. + /// + /// The fully decoded string literal. + private string FinalizeStringLiteral(StringBuilder builder, int bufferedLiteralLength) + { + // Consume everything up to the quote character + string result; + if (builder != null) + { + this.ConsumeTokenAppendToBuilder(builder, bufferedLiteralLength); + result = builder.ToString(); + } + else + { + result = this.ConsumeTokenToString(bufferedLiteralLength); + } + +#if DEBUG + char quoteCharacter = this.characterBuffer[this.tokenStartIndex]; + Debug.Assert(quoteCharacter == '"' || quoteCharacter == '\'', "We should have consumed everything up to the quote character."); +#endif + + // Consume the quote character as well. + this.tokenStartIndex++; + + return result; + } + /// /// Reads more characters from the input. /// @@ -1604,7 +1936,7 @@ private bool ReadInput() /// /// A task that represents the asynchronous operation. /// The value of the TResult parameter contains the node type to report to the user. - private Task ParseValueAsync() + private ValueTask ParseValueAsync() { Debug.Assert( this.tokenStartIndex < this.storedCharacterCount && !IsWhitespaceCharacter(this.characterBuffer[this.tokenStartIndex]), @@ -1621,7 +1953,7 @@ private Task ParseValueAsync() // Start of object this.PushScope(ScopeType.Object); this.tokenStartIndex++; - return CachedTasks.StartObject; + return ValueTask.FromResult(JsonNodeType.StartObject); case '[': // Start of array @@ -1639,7 +1971,7 @@ private Task ParseValueAsync() this.characterBuffer[this.tokenStartIndex] == '\'' || this.characterBuffer[this.tokenStartIndex] == 'n'; - return CachedTasks.StartArray; + return ValueTask.FromResult(JsonNodeType.StartArray); } case '"': @@ -1648,14 +1980,14 @@ private Task ParseValueAsync() // Don't parse yet, as it may be a stream. Defer parsing until .Value is called. this.canStream = true; this.TryPopPropertyScope(); - return CachedTasks.PrimitiveValue; + return ValueTask.FromResult(JsonNodeType.PrimitiveValue); case 'n': // Null value // Don't parse yet, as user may be streaming a stream. Defer parsing until .Value is called. this.canStream = true; this.TryPopPropertyScope(); - return CachedTasks.PrimitiveValue; + return ValueTask.FromResult(JsonNodeType.PrimitiveValue); case 't': case 'f': @@ -1665,7 +1997,7 @@ private Task ParseValueAsync() { this.nodeValue = parseBooleanTask.Result; this.TryPopPropertyScope(); - return CachedTasks.PrimitiveValue; + return ValueTask.FromResult(JsonNodeType.PrimitiveValue); } return AwaitParseBooleanAsync(this, parseBooleanTask); @@ -1680,7 +2012,7 @@ private Task ParseValueAsync() { this.nodeValue = parseNumberTask.Result; this.TryPopPropertyScope(); - return CachedTasks.PrimitiveValue; + return ValueTask.FromResult(JsonNodeType.PrimitiveValue); } return AwaitParseNumberAsync(this, parseNumberTask); @@ -1688,17 +2020,14 @@ private Task ParseValueAsync() else { // Unknown token - fail. - return Task.FromException( + return ValueTask.FromException( JsonReaderExtensions.CreateException(SRResources.JsonReader_UnrecognizedToken)); } } - static async Task AwaitArrayAsync(JsonReader thisParam, ValueTask pendingSkipWhitespacesTask) + static async ValueTask AwaitArrayAsync(JsonReader thisParam, ValueTask pendingSkipWhitespacesTask) { - if (!pendingSkipWhitespacesTask.IsCompletedSuccessfully) - { - await pendingSkipWhitespacesTask.ConfigureAwait(false); - } + await GetOrAwait(pendingSkipWhitespacesTask).ConfigureAwait(false); thisParam.canStream = thisParam.characterBuffer[thisParam.tokenStartIndex] == '"' || @@ -1708,14 +2037,14 @@ static async Task AwaitArrayAsync(JsonReader thisParam, ValueTask< return JsonNodeType.StartArray; } - static async Task AwaitParseBooleanAsync(JsonReader thisParam, ValueTask pendingParseBooleanTask) + static async ValueTask AwaitParseBooleanAsync(JsonReader thisParam, ValueTask pendingParseBooleanTask) { thisParam.nodeValue = await pendingParseBooleanTask.ConfigureAwait(false); thisParam.TryPopPropertyScope(); return JsonNodeType.PrimitiveValue; } - static async Task AwaitParseNumberAsync(JsonReader thisParam, ValueTask pendingParseNumberTask) + static async ValueTask AwaitParseNumberAsync(JsonReader thisParam, ValueTask pendingParseNumberTask) { thisParam.nodeValue = await pendingParseNumberTask.ConfigureAwait(false); thisParam.TryPopPropertyScope(); @@ -1728,7 +2057,7 @@ static async Task AwaitParseNumberAsync(JsonReader thisParam, Valu /// /// A task that represents the asynchronous operation. /// The value of the TResult parameter contains the node type to report to the user. - private async ValueTask ParsePropertyAsync() + private ValueTask ParsePropertyAsync() { // 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."); @@ -1737,29 +2066,93 @@ private async ValueTask ParsePropertyAsync() this.PushScope(ScopeType.Property); // Parse the name of the property - this.nodeValue = await this.ParseNameAsync().ConfigureAwait(false); + ValueTask parseNameTask = this.ParseNameAsync(); + if (!parseNameTask.IsCompletedSuccessfully) + { + return AwaitParseNameAsync(this, parseNameTask); + } - if (string.IsNullOrEmpty((string)this.nodeValue)) + // Completed synchronously + this.nodeValue = parseNameTask.Result; + + return ContinueAfterParseNameAsync(this); + + static ValueTask ContinueAfterParseNameAsync(JsonReader thisParam) { - // The name can't be empty. - throw JsonReaderExtensions.CreateException(Error.Format(SRResources.JsonReader_InvalidPropertyNameOrUnexpectedComma, (string)this.nodeValue)); + if (string.IsNullOrEmpty((string)thisParam.nodeValue)) + { + // The name can't be empty. + return ValueTask.FromException( + JsonReaderExtensions.CreateException( + Error.Format(SRResources.JsonReader_InvalidPropertyNameOrUnexpectedComma, (string)thisParam.nodeValue))); + } + + ValueTask preColonWhitespaceTask = thisParam.SkipWhitespacesAsync(); + if (!preColonWhitespaceTask.IsCompletedSuccessfully) + { + return AwaitPreColonWhitespaceAsync(thisParam, preColonWhitespaceTask); + } + + return ContinueAfterColonAsync(thisParam, preColonWhitespaceTask.Result); } - if (!await this.SkipWhitespacesAsync().ConfigureAwait(false) || this.characterBuffer[this.tokenStartIndex] != ':') + static ValueTask ContinueAfterColonAsync(JsonReader thisParam, bool hasNonWhitespace) { - // We need the colon character after the property name - throw JsonReaderExtensions.CreateException(Error.Format(SRResources.JsonReader_MissingColon, (string)this.nodeValue)); + if (!hasNonWhitespace || thisParam.characterBuffer[thisParam.tokenStartIndex] != ':') + { + // We need the colon character after the property name + return ValueTask.FromException( + JsonReaderExtensions.CreateException( + Error.Format(SRResources.JsonReader_MissingColon, (string)thisParam.nodeValue))); + } + + // Consume the colon. + Debug.Assert(thisParam.characterBuffer[thisParam.tokenStartIndex] == ':', "The above should verify that there's a colon."); + thisParam.tokenStartIndex++; + + ValueTask postColonWhitespaceTask = thisParam.SkipWhitespacesAsync(); + if (!postColonWhitespaceTask.IsCompletedSuccessfully) + { + return AwaitPostColonWhitespaceAsync(thisParam, postColonWhitespaceTask); + } + + return ContinueAfterColonWhitespaceAsync(thisParam); } - // Consume the colon. - Debug.Assert(this.characterBuffer[this.tokenStartIndex] == ':', "The above should verify that there's a colon."); - this.tokenStartIndex++; - await this.SkipWhitespacesAsync().ConfigureAwait(false); + static ValueTask ContinueAfterColonWhitespaceAsync(JsonReader thisParam) + { + // if the content is nested json, we can stream + thisParam.canStream = thisParam.characterBuffer[thisParam.tokenStartIndex] == '{' || thisParam.characterBuffer[thisParam.tokenStartIndex] == '['; - // if the content is nested json, we can stream - this.canStream = this.characterBuffer[this.tokenStartIndex] == '{' || this.characterBuffer[this.tokenStartIndex] == '['; + return ValueTask.FromResult(JsonNodeType.Property); + } - return JsonNodeType.Property; + static async ValueTask AwaitParseNameAsync( + JsonReader thisParam, + ValueTask pendingParseNameTask) + { + thisParam.nodeValue = await pendingParseNameTask.ConfigureAwait(false); + + return await ContinueAfterParseNameAsync(thisParam).ConfigureAwait(false); + } + + static async ValueTask AwaitPreColonWhitespaceAsync( + JsonReader thisParam, + ValueTask pendingPreColonWhitespaceTask) + { + bool nonWhitespaceFound = await GetOrAwait(pendingPreColonWhitespaceTask).ConfigureAwait(false); + + return await ContinueAfterColonAsync(thisParam, nonWhitespaceFound).ConfigureAwait(false); + } + + static async ValueTask AwaitPostColonWhitespaceAsync( + JsonReader thisParam, + ValueTask pendingPostColonWhitespaceTask) + { + await GetOrAwait(pendingPostColonWhitespaceTask).ConfigureAwait(false); + + return await ContinueAfterColonWhitespaceAsync(thisParam); + } } /// @@ -1775,7 +2168,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 ValueTask<(string Value, bool HasLeadingBackslash)> ParseStringPrimitiveValueAsync() { Debug.Assert(this.tokenStartIndex < this.storedCharacterCount, "At least the quote must be present."); @@ -1792,125 +2185,372 @@ private async ValueTask ParsePropertyAsync() // String builder to be used if we need to resolve escape sequences. StringBuilder valueBuilder = null; - int currentCharacterTokenRelativeIndex = 0; - while ((this.tokenStartIndex + currentCharacterTokenRelativeIndex) < this.storedCharacterCount || await this.ReadInputAsync().ConfigureAwait(false)) + int tokenCharOffset = 0; + while (true) { - Debug.Assert((this.tokenStartIndex + currentCharacterTokenRelativeIndex) < this.storedCharacterCount, "ReadInputAsync didn't read more data but returned true."); + // Ensure at least one more character + if (this.tokenStartIndex + tokenCharOffset >= this.storedCharacterCount) + { + // Attempt a non-awaiting async read (may complete synchronously) + ValueTask readInputTask = this.ReadInputAsync(); + if (!readInputTask.IsCompletedSuccessfully) + { + return ParseStringPrimitiveValueResumeAsync(openingQuoteCharacter, valueBuilder, tokenCharOffset, hasLeadingBackslash, readInputTask); + } - char character = this.characterBuffer[this.tokenStartIndex + currentCharacterTokenRelativeIndex]; + // Detect EOF + if (!readInputTask.Result) + { + return ValueTask.FromException<(string, bool)>( + JsonReaderExtensions.CreateException(SRResources.JsonReader_UnexpectedEndOfString)); + } + + Debug.Assert((this.tokenStartIndex + tokenCharOffset) < this.storedCharacterCount, "ReadInputAsync didn't read more data but returned true."); + + // After refill, restart the loop to pick up the character. + continue; + } + + char character = this.characterBuffer[this.tokenStartIndex + tokenCharOffset]; if (character == '\\') { // If we're at the beginning 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) + if (tokenCharOffset == 0 && valueBuilder == null) { hasLeadingBackslash = true; } - // We will need the stringbuilder to resolve the escape sequences. - if (valueBuilder == null) + // We will need the StringBuilder to resolve the escape sequences. + EnsureStringValueBuilderInitialized(ref valueBuilder); + // Append everything up to the \ character to the value. + FlushParsedLiteralToBuilder(valueBuilder, ref tokenCharOffset); + Debug.Assert(this.characterBuffer[this.tokenStartIndex] == '\\', "We should have consumed everything up to the escape character."); + + try { - if (this.stringValueBuilder == null) - { - this.stringValueBuilder = new StringBuilder(); - } - else + // Attempt to process the escape sequence synchronously + if (!this.TryProcessEscapeSequence(valueBuilder)) { - this.stringValueBuilder.Length = 0; + return ProcessEscapeSequenceResumeAsync(openingQuoteCharacter, valueBuilder, tokenCharOffset, hasLeadingBackslash); } - valueBuilder = this.stringValueBuilder; + continue; } - - // Append everything up to the \ character to the value. - this.ConsumeTokenAppendToBuilder(valueBuilder, 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 (!await this.EnsureAvailableCharactersAsync(2).ConfigureAwait(false)) + catch (ODataException ex) { - throw JsonReaderExtensions.CreateException(Error.Format(SRResources.JsonReader_UnrecognizedEscapeSequence, "\\")); + return ValueTask.FromException<(string, bool)>(ex); } + } + else if (character == openingQuoteCharacter) + { + // Consume everything up to the quote character + string result = FinalizeStringLiteral(valueBuilder, tokenCharOffset); - // 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]; + return ValueTask.FromResult((result, hasLeadingBackslash)); + } + else + { + // Normal character, just skip over it - it will become part of the value as is. + tokenCharOffset++; + } + } + } + + /// + /// Ensures references a cleared reusable instance (lazily allocates once). + /// + /// A instance to use for building escaped string literals. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void EnsureStringValueBuilderInitialized(ref StringBuilder builder) + { + if (builder != null) + { + return; + } + + if (this.stringValueBuilder == null) + { + this.stringValueBuilder = new StringBuilder(); + } + else + { + this.stringValueBuilder.Length = 0; + } + + builder = this.stringValueBuilder; + } + + /// + /// Ensures at least characters after the token start index. + /// + /// The number of characters required after the token start index. + /// true if required characters are available; false on EOF or pending asynchronous read. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool TryEnsureAvailableCharacters(int charactersRequiredAfterTokenStart) + { + return this.tokenStartIndex + charactersRequiredAfterTokenStart <= this.storedCharacterCount; + + // NOTE: TryEnsureAvailableCharacters is intentionally a pure availability predicate. + // It currently doesn't trigger I/O but we could potentially call ReadInputAsync here + // to try fetch more data synchronously if required characters are not available. + // HOWEVER, if we did that, we would need to ensure that if ReadInputAsync doesn't complete + // synchronously it is awaited in an async method somewhere else before any other ReadInputAsync calls. + // ReadInputAsync is not re-entrant and we must not call it concurrently. The buffer gets + // corrupted if we do that due to double compaction. + // One approach we could use is maintaining a "pendingReadInputTask" field on the reader + // that is set when ReadInputAsync doesn't complete synchronously and cleared when the task + // is awaited elsewhere. Due to the complexity of managing that state, we leave that as a + // future improvement. + } + + /// + /// Attempts to synchronously process an escape sequence starting at the current '\' position. + /// + /// The instance to use for building escaped string literals. + /// true if the escape sequence was processed successfully; + /// false on EOF or pending asynchronous read. + private bool TryProcessEscapeSequence(StringBuilder builder) + { + Debug.Assert(this.characterBuffer[this.tokenStartIndex] == '\\', "Expected backslash."); + + // Escape sequence - we need at least two characters, the backslash and the one character after it. + if (!this.TryEnsureAvailableCharacters(2)) + { + return false; // Defer to the async path + } + + char escapeChar = this.characterBuffer[this.tokenStartIndex + 1]; + + if (escapeChar == 'u') + { + // For \uXXXX we need 6 characters: '\', 'u' and 4 hex characters. + if (!this.TryEnsureAvailableCharacters(6)) + { + return false; // Defer to the async path + } + + return TryProcessUnicodeEscapeSequence(builder); + } + + // We now know we have enough characters to process the escape sequence synchronously + switch (escapeChar) + { + case 'b': + builder.Append('\b'); + this.tokenStartIndex += 2; + return true; + case 'f': + builder.Append('\f'); + this.tokenStartIndex += 2; + return true; + case 'n': + builder.Append('\n'); + this.tokenStartIndex += 2; + return true; + case 'r': + builder.Append('\r'); + this.tokenStartIndex += 2; + return true; + case 't': + builder.Append('\t'); + this.tokenStartIndex += 2; + return true; + case '\\': + case '\"': + case '\'': + case '/': + builder.Append(escapeChar); this.tokenStartIndex += 2; + return true; + default: + throw JsonReaderExtensions.CreateException(Error.Format(SRResources.JsonReader_UnrecognizedEscapeSequence, "\\" + escapeChar)); + } + } - 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"); + /// + /// Attempts to a '\uXXXX' unicode escape sequence starting at the current '\' position; + /// caller guarantees 4 hex characters are available. + /// + /// The instance to use for building escaped string literals. + /// true if the escape sequence was processed successfully; + /// false on EOF or pending asynchronous read. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool TryProcessUnicodeEscapeSequence(StringBuilder builder) + { + Debug.Assert(this.characterBuffer[this.tokenStartIndex + 1] == 'u', "Expected 'u' after backslash."); - // We need 4 hex characters - if (!await this.EnsureAvailableCharactersAsync(4).ConfigureAwait(false)) - { - throw JsonReaderExtensions.CreateException(Error.Format(SRResources.JsonReader_UnrecognizedEscapeSequence, "\\uXXXX")); - } + // Consume '\' and 'u' + this.tokenStartIndex += 2; - 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)); - } + string unicodeHexValue = this.ConsumeTokenToString(4); + int characterValue; + if (!int.TryParse(unicodeHexValue, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out characterValue)) + { + throw JsonReaderExtensions.CreateException(Error.Format(SRResources.JsonReader_UnrecognizedEscapeSequence, "\\u" + unicodeHexValue)); + } - valueBuilder.Append((char)characterValue); - break; - default: - throw JsonReaderExtensions.CreateException(Error.Format(SRResources.JsonReader_UnrecognizedEscapeSequence, "\\" + character)); - } + builder.Append((char)characterValue); + + return true; + } + + /// + /// Asynchronous slow path string literal parsing when buffer refills or escapes require awaiting. + /// Continues from the first unconsumed character after the opening quote. + /// + /// The quote character for the string literal being parsed. + /// The instance to use for building escaped string literals. + /// Count of buffered literal characters. + /// Indicates if the string has a leading backslash. + /// + /// 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. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private async ValueTask<(string Value, bool HasLeadingBackslash)> ParseStringPrimitiveValueResumeAsync( + char quoteCharacter, + StringBuilder builder, + int bufferedLiteralLength, + bool hasLeadingBackslash, + ValueTask pendingReadInputTask) + { + if (pendingReadInputTask != default) + { + // Detect EOF + if (!await GetOrAwait(pendingReadInputTask).ConfigureAwait(false)) + { + throw JsonReaderExtensions.CreateException(SRResources.JsonReader_UnexpectedEndOfString); } - else if (character == openingQuoteCharacter) + } + + while (true) + { + // Ensure at least one more character + if (this.tokenStartIndex + bufferedLiteralLength >= this.storedCharacterCount) { - // Consume everything up to the quote character - string result; - if (valueBuilder != null) + // Detect EOF + if (!await this.ReadInputAsync().ConfigureAwait(false)) { - this.ConsumeTokenAppendToBuilder(valueBuilder, currentCharacterTokenRelativeIndex); - result = valueBuilder.ToString(); + throw JsonReaderExtensions.CreateException(SRResources.JsonReader_UnexpectedEndOfString); } - else + } + + char ch = this.characterBuffer[this.tokenStartIndex + bufferedLiteralLength]; + if (ch == '\\') + { + // If we're at the beginning of the string + // (means that relative token index must be 0 and we must not have consumed anything into our value builder yet) + if (bufferedLiteralLength == 0 && builder == null) { - result = this.ConsumeTokenToString(currentCharacterTokenRelativeIndex); + hasLeadingBackslash = true; } - Debug.Assert(this.characterBuffer[this.tokenStartIndex] == openingQuoteCharacter, "We should have consumed everything up to the quote character."); + // We will need the StringBuilder to resolve the escape sequences. + EnsureStringValueBuilderInitialized(ref builder); + // Append everything up to the \ character to the value. + FlushParsedLiteralToBuilder(builder, ref bufferedLiteralLength); + await ProcessEscapeSequenceCoreAsync(builder).ConfigureAwait(false); - // Consume the quote character as well. - this.tokenStartIndex++; + continue; + } + else if (ch == quoteCharacter) + { + // Consume everything up to the quote character + string result = FinalizeStringLiteral(builder, bufferedLiteralLength); return (result, hasLeadingBackslash); } else { // Normal character, just skip over it - it will become part of the value as is. - currentCharacterTokenRelativeIndex++; + bufferedLiteralLength++; } } + } - throw JsonReaderExtensions.CreateException(SRResources.JsonReader_UnexpectedEndOfString); + /// + /// Asynchronous slow path for processing an escape sequence that cannot be completed synchronously. + /// + /// The quote character for the string literal being parsed. + /// The instance to use for building escaped string literals. + /// Count of buffered literal characters. + /// Indicates if the string has a leading backslash. + /// + /// 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. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private async ValueTask<(string Value, bool HasLeadingBackslash)> ProcessEscapeSequenceResumeAsync( + char quoteCharacter, + StringBuilder builder, + int bufferedLiteralLength, + bool hasLeadingBackslash) + { + Debug.Assert(this.characterBuffer[this.tokenStartIndex] == '\\', "We should have consumed everything up to the escape character."); + Debug.Assert(builder != null, "builder must be initialized prior to asynchronous processing of escape sequences."); + + await ProcessEscapeSequenceCoreAsync(builder).ConfigureAwait(false); + + // Continue with asynchronous literal parsing + return await ParseStringPrimitiveValueResumeAsync(quoteCharacter, builder, 0, hasLeadingBackslash, default).ConfigureAwait(false); + } + + /// + /// Asynchronously processes an escape sequence. + /// + /// The instance to use for building escaped string literals. + /// A task that represents the asynchronous operation. + [MethodImpl(MethodImplOptions.NoInlining)] + private async ValueTask ProcessEscapeSequenceCoreAsync(StringBuilder builder) + { + if (!await this.EnsureAvailableCharactersAsync(2).ConfigureAwait(false)) + { + throw JsonReaderExtensions.CreateException( + Error.Format(SRResources.JsonReader_UnrecognizedEscapeSequence, "\\")); + } + + char escapeChar = this.characterBuffer[this.tokenStartIndex + 1]; + + if (escapeChar == 'u') + { + if (!await this.EnsureAvailableCharactersAsync(6).ConfigureAwait(false)) + { + string badUnicodeHexValue = new string(this.characterBuffer, this.tokenStartIndex, this.storedCharacterCount - this.tokenStartIndex); + throw JsonReaderExtensions.CreateException( + Error.Format(SRResources.JsonReader_UnrecognizedEscapeSequence, badUnicodeHexValue)); + } + + // Consume '\' and 'u' + this.tokenStartIndex += 2; + + string unicodeHexValue = this.ConsumeTokenToString(4); + int characterValue; + if (!int.TryParse(unicodeHexValue, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out characterValue)) + { + throw JsonReaderExtensions.CreateException(Error.Format(SRResources.JsonReader_UnrecognizedEscapeSequence, "\\u" + unicodeHexValue)); + } + + builder.Append((char)characterValue); + + return; + } + + // Non-unicode escape sequence + this.tokenStartIndex += 2; + builder.Append(escapeChar switch + { + 'b' => '\b', + 'f' => '\f', + 'n' => '\n', + 'r' => '\r', + 't' => '\t', + '\\' or '\"' or '\'' or '/' => escapeChar, + _ => throw JsonReaderExtensions.CreateException( + Error.Format(SRResources.JsonReader_UnrecognizedEscapeSequence, "\\" + escapeChar)) + }); } /// @@ -2581,7 +3221,7 @@ private int ParseUnicodeHexValue() { string unicodeHexValue = this.ConsumeTokenToString(4); int characterValue; - if (!Int32.TryParse(unicodeHexValue, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out characterValue)) + if (!int.TryParse(unicodeHexValue, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out characterValue)) { throw JsonReaderExtensions.CreateException(Error.Format(SRResources.JsonReader_UnrecognizedEscapeSequence, "\\u" + unicodeHexValue)); } @@ -2615,7 +3255,7 @@ private object ParseNumericToken(string numberString) // 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 (int.TryParse(numberString, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out int intValue)) { return intValue; } diff --git a/src/Microsoft.OData.Core/Json/JsonReaderExtensions.cs b/src/Microsoft.OData.Core/Json/JsonReaderExtensions.cs index 84a997b0bc..0b9e5dd53e 100644 --- a/src/Microsoft.OData.Core/Json/JsonReaderExtensions.cs +++ b/src/Microsoft.OData.Core/Json/JsonReaderExtensions.cs @@ -6,14 +6,15 @@ namespace Microsoft.OData.Json { - using Microsoft.OData.Core; using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; + using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; + using Microsoft.OData.Core; /// /// Extension methods for the JSON reader. @@ -174,25 +175,8 @@ internal static string ReadStringValue(this IJsonReader jsonReader, string prope Debug.Assert(jsonReader != null, "jsonReader != null"); object value = jsonReader.ReadPrimitiveValue(); - double? doubleValue = value as double?; - if (value == null || doubleValue != null) - { - return doubleValue; - } - - int? intValue = value as int?; - if (intValue != null) - { - return (double)intValue; - } - - decimal? decimalValue = value as decimal?; - if (decimalValue != null) - { - return (double)decimalValue; - } - - throw CreateException(Error.Format(SRResources.JsonReaderExtensions_CannotReadValueAsDouble, value)); + + return CoerceToNullableDouble(value); } /// @@ -207,27 +191,10 @@ 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; - } + depth = AdjustDepth(depth, jsonReader.NodeType); } while (jsonReader.Read() && depth > 0); @@ -235,7 +202,7 @@ internal static void SkipValue(this IJsonReader jsonReader) { // 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(SRResources.JsonReader_EndOfInputWithOpenScope); + throw CreateException(SRResources.JsonReader_EndOfInputWithOpenScope); } } @@ -309,7 +276,7 @@ internal static void SkipValue(this IJsonReader jsonReader, StringBuilder jsonRa { // 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(SRResources.JsonReader_EndOfInputWithOpenScope); + throw CreateException(SRResources.JsonReader_EndOfInputWithOpenScope); } jsonWriter.Flush(); @@ -465,7 +432,7 @@ internal static bool IsOnValueNode(this IJsonReader jsonReader) /// /// The to read from. /// A task that represents the asynchronous read operation. - internal static Task ReadStartObjectAsync(this IJsonReader jsonReader) + internal static ValueTask ReadStartObjectAsync(this IJsonReader jsonReader) { Debug.Assert(jsonReader != null, "jsonReader != null"); @@ -477,7 +444,7 @@ internal static Task ReadStartObjectAsync(this IJsonReader jsonReader) /// /// The to read from. /// A task that represents the asynchronous read operation. - internal static Task ReadEndObjectAsync(this IJsonReader jsonReader) + internal static ValueTask ReadEndObjectAsync(this IJsonReader jsonReader) { Debug.Assert(jsonReader != null, "jsonReader != null"); @@ -489,7 +456,7 @@ internal static Task ReadEndObjectAsync(this IJsonReader jsonReader) /// /// The to read from. /// A task that represents the asynchronous read operation. - internal static Task ReadStartArrayAsync(this IJsonReader jsonReader) + internal static ValueTask ReadStartArrayAsync(this IJsonReader jsonReader) { Debug.Assert(jsonReader != null, "jsonReader != null"); @@ -501,7 +468,7 @@ internal static Task ReadStartArrayAsync(this IJsonReader jsonReader) /// /// The to read from. /// A task that represents the asynchronous read operation. - internal static Task ReadEndArrayAsync(this IJsonReader jsonReader) + internal static ValueTask ReadEndArrayAsync(this IJsonReader jsonReader) { Debug.Assert(jsonReader != null, "jsonReader != null"); @@ -514,15 +481,26 @@ internal static Task ReadEndArrayAsync(this IJsonReader jsonReader) /// The to read from. /// A task that represents the asynchronous read operation. /// The value of the TResult parameter contains the property name of the current property node. - internal static async Task GetPropertyNameAsync(this IJsonReader jsonReader) + internal static Task GetPropertyNameAsync(this IJsonReader jsonReader) { Debug.Assert(jsonReader != null, "jsonReader != null"); Debug.Assert(jsonReader.NodeType == JsonNodeType.Property, "jsonReader.NodeType == JsonNodeType.Property"); // NOTE: the JSON reader already verifies that property names are strings and not null/empty - object value = await jsonReader.GetValueAsync() - .ConfigureAwait(false); - return (string)value; + Task getValueTask = jsonReader.GetValueAsync(); + if (getValueTask.IsCompletedSuccessfully) + { + return Task.FromResult((string)getValueTask.Result); + } + + return AwaitGetValueAsync(getValueTask); + + static async Task AwaitGetValueAsync(Task pendingGetValueTask) + { + object value = await pendingGetValueTask.ConfigureAwait(false); + + return (string)value; + } } /// @@ -531,17 +509,49 @@ internal static async Task GetPropertyNameAsync(this IJsonReader jsonRea /// The to read from. /// A task that represents the asynchronous read operation. /// The value of the TResult parameter contains the property name of the property node read. - internal static async Task ReadPropertyNameAsync(this IJsonReader jsonReader) + internal static Task ReadPropertyNameAsync(this IJsonReader jsonReader) { Debug.Assert(jsonReader != null, "jsonReader != null"); - jsonReader.ValidateNodeType(JsonNodeType.Property); - string propertyName = await jsonReader.GetPropertyNameAsync() - .ConfigureAwait(false); - await jsonReader.ReadNextAsync() - .ConfigureAwait(false); + try + { + jsonReader.ValidateNodeType(JsonNodeType.Property); + } + catch (ODataException ex) + { + return Task.FromException(ex); + } - return propertyName; + Task getPropertyNameTask = jsonReader.GetPropertyNameAsync(); + if (getPropertyNameTask.IsCompletedSuccessfully) + { + string propertyName = getPropertyNameTask.Result; + Task readNextTask = jsonReader.ReadNextAsync(); + if (readNextTask.IsCompletedSuccessfully) + { + return Task.FromResult(propertyName); ; + } + + return AwaitReadNextAsync(readNextTask, propertyName); + } + + return AwaitGetPropertyNameAsync(jsonReader, getPropertyNameTask); + + static async Task AwaitReadNextAsync(Task pendingReadNextTask, string propertyName) + { + await pendingReadNextTask.ConfigureAwait(false); + + return propertyName; + } + + static async Task AwaitGetPropertyNameAsync(IJsonReader localJsonReader, Task pendingGetPropertyNameTask) + { + string propertyName = await pendingGetPropertyNameTask.ConfigureAwait(false); + await localJsonReader.ReadNextAsync() + .ConfigureAwait(false); + + return propertyName; + } } /// @@ -550,16 +560,40 @@ await jsonReader.ReadNextAsync() /// The to read from. /// A task that represents the asynchronous read operation. /// The value of the TResult parameter contains the primitive value read from the reader. - internal static async Task ReadPrimitiveValueAsync(this IJsonReader jsonReader) + internal static Task ReadPrimitiveValueAsync(this IJsonReader jsonReader) { Debug.Assert(jsonReader != null, "jsonReader != null"); - object value = await jsonReader.GetValueAsync() - .ConfigureAwait(false); - await ReadNextAsync(jsonReader, JsonNodeType.PrimitiveValue) - .ConfigureAwait(false); + Task getValueTask = jsonReader.GetValueAsync(); + if (getValueTask.IsCompletedSuccessfully) + { + object value = getValueTask.Result; + ValueTask readNextTask = ReadNextAsync(jsonReader, JsonNodeType.PrimitiveValue); + if (readNextTask.IsCompletedSuccessfully) + { + return Task.FromResult(value); + } + + return AwaitReadNextAsync(readNextTask, value); + } - return value; + return AwaitGetValueAsync(jsonReader, getValueTask); + + static async Task AwaitReadNextAsync(ValueTask pendingReadNextTask, object value) + { + await pendingReadNextTask.ConfigureAwait(false); + + return value; + } + + static async Task AwaitGetValueAsync(IJsonReader localJsonReader, Task pendingGetValueTask) + { + object value = await pendingGetValueTask.ConfigureAwait(false); + await ReadNextAsync(localJsonReader, JsonNodeType.PrimitiveValue) + .ConfigureAwait(false); + + return value; + } } /// @@ -570,26 +604,48 @@ await ReadNextAsync(jsonReader, JsonNodeType.PrimitiveValue) /// A task that represents the asynchronous read operation. /// The value of the TResult parameter contains the string value read from the reader; /// throws an exception if no string value could be read. - internal static async Task ReadStringValueAsync(this IJsonReader jsonReader, string propertyName = null) + internal static Task ReadStringValueAsync(this IJsonReader jsonReader, string propertyName = null) { Debug.Assert(jsonReader != null, "jsonReader != null"); - object value = await jsonReader.ReadPrimitiveValueAsync() - .ConfigureAwait(false); - - string stringValue = value as string; - if (value == null || stringValue != null) + Task readPrimitiveValueTask = jsonReader.ReadPrimitiveValueAsync(); + if (readPrimitiveValueTask.IsCompletedSuccessfully) { - return stringValue; + try + { + return Task.FromResult(ProcessResult(readPrimitiveValueTask.Result, propertyName)); + } + catch (ODataException ex) + { + return Task.FromException(ex); + } } - if (!string.IsNullOrEmpty(propertyName)) + return AwaitReadPrimitiveValueAsync(readPrimitiveValueTask, propertyName); + + static string ProcessResult(object value, string propertyName) { - throw CreateException(Error.Format(SRResources.JsonReaderExtensions_CannotReadPropertyValueAsString, value, propertyName)); + string stringValue = value as string; + if (value == null || stringValue != null) + { + return stringValue; + } + + if (!string.IsNullOrEmpty(propertyName)) + { + throw CreateException(Error.Format(SRResources.JsonReaderExtensions_CannotReadPropertyValueAsString, value, propertyName)); + } + else + { + throw CreateException(Error.Format(SRResources.JsonReaderExtensions_CannotReadValueAsString, value)); + } } - else + + static async Task AwaitReadPrimitiveValueAsync(Task pendingReadPrimitiveValueTask, string propertyName) { - throw CreateException(Error.Format(SRResources.JsonReaderExtensions_CannotReadValueAsString, value)); + object value = await pendingReadPrimitiveValueTask.ConfigureAwait(false); + + return ProcessResult(value, propertyName); } } @@ -600,10 +656,22 @@ internal static async Task ReadStringValueAsync(this IJsonReader jsonRea /// A task that represents the asynchronous read operation. /// The value of the TResult parameter contains the string value read from the reader as a URI; /// throws an exception if no string value could be read. - internal static async Task ReadUriValueAsync(this IJsonReader jsonReader) + internal static Task ReadUriValueAsync(this IJsonReader jsonReader) { - return UriUtils.StringToUri(await ReadStringValueAsync(jsonReader) - .ConfigureAwait(false)); + Task readStringValueTask = ReadStringValueAsync(jsonReader); + if (readStringValueTask.IsCompletedSuccessfully) + { + return Task.FromResult(UriUtils.StringToUri(readStringValueTask.Result)); + } + + return AwaitReadStringValueAsync(readStringValueTask); + + static async Task AwaitReadStringValueAsync(Task pendingReadStringValueTask) + { + string stringValue = await pendingReadStringValueTask.ConfigureAwait(false); + + return UriUtils.StringToUri(stringValue); + } } /// @@ -613,32 +681,31 @@ internal static async Task ReadUriValueAsync(this IJsonReader jsonReader) /// A task that represents the asynchronous read operation. /// The value of the TResult parameter contains the double value read from the reader; /// throws an exception if no double value could be read. - internal static async Task ReadDoubleValueAsync(this IJsonReader jsonReader) + internal static Task ReadDoubleValueAsync(this IJsonReader jsonReader) { Debug.Assert(jsonReader != null, "jsonReader != null"); - object value = await jsonReader.ReadPrimitiveValueAsync() - .ConfigureAwait(false); - - double? doubleValue = value as double?; - if (value == null || doubleValue != null) + Task readPrimitiveValueTask = jsonReader.ReadPrimitiveValueAsync(); + if (readPrimitiveValueTask.IsCompletedSuccessfully) { - return doubleValue; + try + { + return Task.FromResult(CoerceToNullableDouble(readPrimitiveValueTask.Result)); + } + catch (ODataException ex) + { + return Task.FromException(ex); + } } - int? intValue = value as int?; - if (intValue != null) - { - return (double)intValue; - } + return AwaitReadPrimitiveValueAsync(readPrimitiveValueTask); - decimal? decimalValue = value as decimal?; - if (decimalValue != null) + static async Task AwaitReadPrimitiveValueAsync(Task pendingReadPrimitiveValueTask) { - return (double)decimalValue; - } + object value = await pendingReadPrimitiveValueTask.ConfigureAwait(false); - throw CreateException(Error.Format(SRResources.JsonReaderExtensions_CannotReadValueAsDouble, value)); + return CoerceToNullableDouble(value); + } } /// @@ -650,40 +717,71 @@ internal static async Task ReadUriValueAsync(this IJsonReader jsonReader) /// Post-Condition: JsonNodeType.PrimitiveValue, JsonNodeType.EndArray or JsonNodeType.EndObject /// /// A task that represents the asynchronous read operation. - internal static async Task SkipValueAsync(this IJsonReader jsonReader) + internal static Task SkipValueAsync(this IJsonReader jsonReader) { Debug.Assert(jsonReader != null, "jsonReader != null"); int depth = 0; - do + while (true) { - switch (jsonReader.NodeType) + // Adjust for the current node before advancing + depth = AdjustDepth(depth, jsonReader.NodeType); + + Task readTask = jsonReader.ReadAsync(); + if (!readTask.IsCompletedSuccessfully) { - case JsonNodeType.StartArray: - case JsonNodeType.StartObject: - depth++; - break; + return AwaitReadAsync(jsonReader, depth, readTask); + } - case JsonNodeType.EndArray: - case JsonNodeType.EndObject: - Debug.Assert(depth > 0, "Seen too many scope ends."); - depth--; - break; + // End of input + if (!readTask.Result) + { + 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; + if (depth == 0) + { + // Closed the entire value and advanced past it + return Task.CompletedTask; } } - while (await jsonReader.ReadAsync().ConfigureAwait(false) && 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 CreateException(SRResources.JsonReader_EndOfInputWithOpenScope); + return Task.FromException(CreateException(SRResources.JsonReader_EndOfInputWithOpenScope)); + } + + // Edge case: value ended exactly at end of input with depth 0 + return Task.CompletedTask; + + static async Task AwaitReadAsync(IJsonReader localJsonReader, int depth, Task pendingReadTask) + { + while (true) + { + bool isReadSuccessfully = await pendingReadTask.ConfigureAwait(false); + if (!isReadSuccessfully) + { + break; + } + + // Finished (depth hit zero before advancing) - but we already advanced past the value + if (depth == 0) + { + return; + } + + // Process next node + depth = AdjustDepth(depth, localJsonReader.NodeType); + + pendingReadTask = localJsonReader.ReadAsync(); + } + + if (depth > 0) + { + // Not all open scopes were closed: + throw CreateException(SRResources.JsonReader_EndOfInputWithOpenScope); + } } } @@ -781,17 +879,30 @@ await jsonWriter.FlushAsync() /// The reader to inspect. /// A task that represents the asynchronous read operation. /// The value of the TResult parameter contains the value read from the reader. - internal static async Task ReadAsUntypedOrNullValueAsync(this IJsonReader jsonReader) + internal static Task ReadAsUntypedOrNullValueAsync(this IJsonReader jsonReader) { StringBuilder builder = new StringBuilder(); - await jsonReader.SkipValueAsync(builder) - .ConfigureAwait(false); - Debug.Assert(builder.Length > 0, "builder.Length > 0"); + Task skipValueTask = jsonReader.SkipValueAsync(builder); + if (skipValueTask.IsCompletedSuccessfully) + { + Debug.Assert(builder.Length > 0, "builder.Length > 0"); + return Task.FromResult(new ODataUntypedValue() + { + RawValue = builder.ToString(), + }); + } - return new ODataUntypedValue() + return AwaitSkipValueAsync(skipValueTask, builder); + + static async Task AwaitSkipValueAsync(Task pendingSkipValueTask, StringBuilder localBuilder) { - RawValue = builder.ToString(), - }; + await pendingSkipValueTask.ConfigureAwait(false); + Debug.Assert(localBuilder.Length > 0, "localBuilder.Length > 0"); + return new ODataUntypedValue() + { + RawValue = localBuilder.ToString(), + }; + } } /// @@ -800,63 +911,21 @@ await jsonReader.SkipValueAsync(builder) /// The reader to inspect. /// A task that represents the asynchronous read operation. /// The value of the TResult parameter contains the value read from the reader. - internal static async Task ReadODataValueAsync(this IJsonReader jsonReader) + internal static Task ReadODataValueAsync(this IJsonReader jsonReader) { - if (jsonReader.NodeType == JsonNodeType.PrimitiveValue) - { - object primitiveValue = await jsonReader.ReadPrimitiveValueAsync() - .ConfigureAwait(false); - - return primitiveValue.ToODataValue(); - } - else if (jsonReader.NodeType == JsonNodeType.StartObject) - { - await jsonReader.ReadStartObjectAsync() - .ConfigureAwait(false); - ODataResourceValue resourceValue = new ODataResourceValue(); - List properties = new List(); - - while (jsonReader.NodeType != JsonNodeType.EndObject) - { - ODataProperty property = new ODataProperty(); - property.Name = await jsonReader.ReadPropertyNameAsync() - .ConfigureAwait(false); - property.Value = await jsonReader.ReadODataValueAsync() - .ConfigureAwait(false); - properties.Add(property); - } - - resourceValue.Properties = properties; - - await jsonReader.ReadEndObjectAsync() - .ConfigureAwait(false); - - return resourceValue; - } - else if (jsonReader.NodeType == JsonNodeType.StartArray) + switch (jsonReader.NodeType) { - await jsonReader.ReadStartArrayAsync() - .ConfigureAwait(false); - ODataCollectionValue collectionValue = new ODataCollectionValue(); - List properties = new List(); + case JsonNodeType.PrimitiveValue: + return jsonReader.ReadODataPrimitiveValueAsync(); - while (jsonReader.NodeType != JsonNodeType.EndArray) - { - ODataValue odataValue = await jsonReader.ReadODataValueAsync() - .ConfigureAwait(false); - properties.Add(odataValue); - } + case JsonNodeType.StartObject: + return jsonReader.ReadODataResourceValueAsync(); - collectionValue.Items = properties; - await jsonReader.ReadEndArrayAsync() - .ConfigureAwait(false); + case JsonNodeType.StartArray: + return jsonReader.ReadODataCollectionValueAsync(); - return collectionValue; - } - else - { - return await jsonReader.ReadAsUntypedOrNullValueAsync() - .ConfigureAwait(false); + default: + return jsonReader.ReadAsUntypedOrNullValueAsync(); } } @@ -866,19 +935,30 @@ await jsonReader.ReadEndArrayAsync() /// The to read from. /// A task that represents the asynchronous read operation. /// The value of the TResult parameter contains the node type of the node that reader is positioned on after reading. - internal static async Task ReadNextAsync(this IJsonReader jsonReader) + internal static Task ReadNextAsync(this IJsonReader jsonReader) { Debug.Assert(jsonReader != null, "jsonReader != null"); + Task readTask = jsonReader.ReadAsync(); + if (readTask.IsCompletedSuccessfully) + { #if DEBUG - bool result = await jsonReader.ReadAsync() - .ConfigureAwait(false); - Debug.Assert(result, "JsonReader.ReadAsync returned false in an unexpected place."); -#else - await jsonReader.ReadAsync() - .ConfigureAwait(false); + Debug.Assert(readTask.Result, "JsonReader.ReadAsync returned false in an unexpected place."); #endif - return jsonReader.NodeType; + return Task.FromResult(jsonReader.NodeType); + } + + return AwaitReadAsync(readTask, jsonReader); + + static async Task AwaitReadAsync(Task pendingReadTask, IJsonReader localJsonReader) + { + bool result = await pendingReadTask.ConfigureAwait(false); + +#if DEBUG + Debug.Assert(result, "JsonReader.ReadAsync returned false in an unexpected place."); +#endif + return localJsonReader.NodeType; + } } /// @@ -911,6 +991,7 @@ internal static void AssertBuffering(this BufferingJsonReader bufferedJsonReader /// /// String to use for the exception message. /// Exception to be thrown. + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static ODataException CreateException(string exceptionMessage) { return new ODataException(exceptionMessage); @@ -941,6 +1022,7 @@ private static void ReadNext(this IJsonReader jsonReader, JsonNodeType expectedN /// /// Thrown if the of is not /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void ValidateNodeType(this IJsonReader jsonReader, JsonNodeType expectedNodeType) { Debug.Assert(jsonReader != null, "jsonReader != null"); @@ -957,13 +1039,125 @@ private static void ValidateNodeType(this IJsonReader jsonReader, JsonNodeType e /// /// The to read from. /// The expected of the read node. - private static async Task ReadNextAsync(this IJsonReader jsonReader, JsonNodeType expectedNodeType) + private static ValueTask ReadNextAsync(this IJsonReader jsonReader, JsonNodeType expectedNodeType) { Debug.Assert(jsonReader != null, "jsonReader != null"); Debug.Assert(expectedNodeType != JsonNodeType.None, "expectedNodeType != JsonNodeType.None"); - jsonReader.ValidateNodeType(expectedNodeType); - await jsonReader.ReadAsync().ConfigureAwait(false); + try + { + jsonReader.ValidateNodeType(expectedNodeType); + } + catch (ODataException ex) + { + return ValueTask.FromException(ex); + } + + Task readTask = jsonReader.ReadAsync(); + if (readTask.IsCompletedSuccessfully) + { + return ValueTask.CompletedTask; + } + + return AwaitReadAsync(readTask); + + static async ValueTask AwaitReadAsync(Task pendingReadTask) + { + await pendingReadTask.ConfigureAwait(false); + } + } + + /// + /// Asynchronously reads a primitive value, and converts + /// the raw CLR value to an . + /// + /// The positioned on a PrimitiveValue node. + /// A task that represents the asynchronous read operation. + /// The value of the TResult parameter contains the . + private static Task ReadODataPrimitiveValueAsync(this IJsonReader jsonReader) + { + Debug.Assert(jsonReader != null, "jsonReader != null"); + + Task readPrimitiveValueTask = jsonReader.ReadPrimitiveValueAsync(); + if (readPrimitiveValueTask.IsCompletedSuccessfully) + { + object primitiveValue = readPrimitiveValueTask.Result; + + return Task.FromResult(primitiveValue.ToODataValue()); + } + + return AwaitReadPrimitiveValueAsync(readPrimitiveValueTask); + + static async Task AwaitReadPrimitiveValueAsync(Task pendingReadPrimitiveValueTask) + { + object primitiveValue = await pendingReadPrimitiveValueTask.ConfigureAwait(false); + + return primitiveValue.ToODataValue(); + } + } + + /// + /// Asynchronously reads an object value, and materializes + /// it as an . + /// + /// The positioned on a StartObject node. + /// A task that represents the asynchronous read operation. + /// The value of the TResult parameter contains the . + private static async Task ReadODataResourceValueAsync(this IJsonReader jsonReader) + { + Debug.Assert(jsonReader != null, "jsonReader != null"); + + await jsonReader.ReadStartObjectAsync() + .ConfigureAwait(false); + ODataResourceValue resourceValue = new ODataResourceValue(); + List properties = new List(); + + while (jsonReader.NodeType != JsonNodeType.EndObject) + { + ODataProperty property = new ODataProperty(); + property.Name = await jsonReader.ReadPropertyNameAsync() + .ConfigureAwait(false); + property.Value = await jsonReader.ReadODataValueAsync() + .ConfigureAwait(false); + properties.Add(property); + } + + resourceValue.Properties = properties; + + await jsonReader.ReadEndObjectAsync() + .ConfigureAwait(false); + + return resourceValue; + } + + /// + /// Asynchronously reads an array value, and materializes + /// it as an . + /// + /// The positioned on a StartArray node. + /// A task that represents the asynchronous read operation. + /// The value of the TResult parameter contains the . + private static async Task ReadODataCollectionValueAsync(this IJsonReader jsonReader) + { + Debug.Assert(jsonReader != null, "jsonReader != null"); + + await jsonReader.ReadStartArrayAsync() + .ConfigureAwait(false); + ODataCollectionValue collectionValue = new ODataCollectionValue(); + List properties = new List(); + + while (jsonReader.NodeType != JsonNodeType.EndArray) + { + ODataValue odataValue = await jsonReader.ReadODataValueAsync() + .ConfigureAwait(false); + properties.Add(odataValue); + } + + collectionValue.Items = properties; + await jsonReader.ReadEndArrayAsync() + .ConfigureAwait(false); + + return collectionValue; } /// @@ -971,9 +1165,71 @@ private static async Task ReadNextAsync(this IJsonReader jsonReader, JsonNodeTyp /// /// The node type. /// true if the node type is PrimitiveValue, StartObject or StartArray node; false otherwise. + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsValueNodeType(JsonNodeType nodeType) { return nodeType == JsonNodeType.PrimitiveValue || nodeType == JsonNodeType.StartObject || nodeType == JsonNodeType.StartArray; } + + /// + /// Converts a boxed numeric (double, int, decimal) or null to a nullable double; + /// throws if the value is a non-null unsupported type. + /// + /// The boxed numeric or null. + /// The coerced double value or null. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double? CoerceToNullableDouble(object value) + { + if (value == null) + { + return null; + } + + if (value is double d) + { + return d; + } + + if (value is int i) + { + return (double)i; + } + + if (value is decimal m) + { + return (double)m; + } + + throw CreateException(Error.Format(SRResources.JsonReaderExtensions_CannotReadValueAsDouble, value)); + } + + /// + /// Adjusts the running nesting depth counter based on the current node type, + /// incrementing for start scopes and decrementing for end scopes. + /// + /// The current nesting depth. + /// The node just encountered. + /// The updated depth. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int AdjustDepth(int currentDepth, JsonNodeType nodeType) + { + switch (nodeType) + { + case JsonNodeType.StartArray: + case JsonNodeType.StartObject: + return currentDepth + 1; + + case JsonNodeType.EndArray: + case JsonNodeType.EndObject: + Debug.Assert(currentDepth > 0, "Seen too many scope ends."); + return currentDepth - 1; + + default: + Debug.Assert( + nodeType != JsonNodeType.EndOfInput, + "Unexpected EndOfInput inside SkipValueAsync depth traversal."); + return currentDepth; + } + } } } diff --git a/src/Microsoft.OData.Core/Json/ReorderingJsonReader.cs b/src/Microsoft.OData.Core/Json/ReorderingJsonReader.cs index f9f16d7706..9415b34b29 100644 --- a/src/Microsoft.OData.Core/Json/ReorderingJsonReader.cs +++ b/src/Microsoft.OData.Core/Json/ReorderingJsonReader.cs @@ -12,6 +12,7 @@ namespace Microsoft.OData.Json using System.Collections.Generic; using System.Diagnostics; using System.IO; + using System.Runtime.CompilerServices; using System.Threading.Tasks; #endregion Namespaces @@ -155,12 +156,23 @@ await this.ReadAsync() /// A task that represents the asynchronous operation. /// The value of the TResult parameter contains true if the current value can be streamed; otherwise false. /// - public override async Task CanStreamAsync() + public override Task CanStreamAsync() { - object value = await this.GetValueAsync() - .ConfigureAwait(false); + Task getValueTask = this.GetValueAsync(); + if (getValueTask.IsCompletedSuccessfully) + { + object value = getValueTask.Result; + return (value is string || value == null) ? CachedTasks.True : CachedTasks.False; + } + + return AwaitGetValueAsync(getValueTask); - return value is string || value == null; + static async Task AwaitGetValueAsync(Task pendingGetValueTask) + { + object value = await pendingGetValueTask.ConfigureAwait(false); + + return value is string || value == null; + } } /// @@ -282,84 +294,217 @@ protected override async Task ProcessObjectValueAsync() switch (this.currentBufferedNode.NodeType) { case JsonNodeType.StartObject: + await this.HandleStartObjectAsync(bufferedObjectStack) + .ConfigureAwait(false); + + break; + case JsonNodeType.EndObject: + if (await this.HandleEndObjectAsync(bufferedObjectStack) + .ConfigureAwait(false)) { - // New object record - add the node to our stack - BufferedObject bufferedObject = new BufferedObject { ObjectStart = this.currentBufferedNode }; - bufferedObjectStack.Push(bufferedObject); + // Processed outermost object + return; + } - // See if it's an in-stream error - await base.ProcessObjectValueAsync() - .ConfigureAwait(false); - this.currentBufferedNode = bufferedObject.ObjectStart; + break; + case JsonNodeType.Property: + await this.HandlePropertyAsync(bufferedObjectStack) + .ConfigureAwait(false); - await this.ReadInternalAsync() - .ConfigureAwait(false); - } + break; + default: + // Read over (buffer) everything else + await this.ReadInternalAsync() + .ConfigureAwait(false); break; - case JsonNodeType.EndObject: - { - // End of object record - // Pop the node from our stack - BufferedObject bufferedObject = bufferedObjectStack.Pop(); + } + } + } - // If there is a previous property record, mark its last value node. - if (bufferedObject.CurrentProperty != null) - { - bufferedObject.CurrentProperty.EndOfPropertyValueNode = this.currentBufferedNode.Previous; - } + /// + /// Processes a start object: pushes a new frame, probes for in-stream error via base, + /// then advances to the first child (property or EndObject). + /// + /// A ValueTask that completes when positioned on the first child of the new object. + private ValueTask HandleStartObjectAsync(Stack bufferedObjectStack) + { + // New object record - add the node to our stack + BufferedObject bufferedObject = new BufferedObject { ObjectStart = this.currentBufferedNode }; + bufferedObjectStack.Push(bufferedObject); - // Now perform the re-ordering on the buffered nodes - bufferedObject.Reorder(); + // See if it's an in-stream error + Task processObjectValueTask = base.ProcessObjectValueAsync(); + if (!processObjectValueTask.IsCompletedSuccessfully) + { + return AwaitProcessObjectValueAsync( + this, + bufferedObject, + processObjectValueTask); + } - if (bufferedObjectStack.Count == 0) - { - // No more objects to process - we're done. - return; - } + // Completed synchronously + this.currentBufferedNode = bufferedObject.ObjectStart; - await this.ReadInternalAsync() - .ConfigureAwait(false); - } + // Advance into the first child node (Property or EndObject) + Task readInternalTask = this.ReadInternalAsync(); + if (readInternalTask.IsCompletedSuccessfully) + { + return ValueTask.CompletedTask; + } - break; - case JsonNodeType.Property: - { - BufferedObject bufferedObject = bufferedObjectStack.Peek(); + return AwaitReadInternalAsync(this, readInternalTask); - // If there is a current property, mark its last value node. - if (bufferedObject.CurrentProperty != null) - { - bufferedObject.CurrentProperty.EndOfPropertyValueNode = this.currentBufferedNode.Previous; - } + static async ValueTask AwaitProcessObjectValueAsync( + ReorderingJsonReader thisParam, + BufferedObject bufferedObject, + Task pendingProcessObjectValueTask) + { + await pendingProcessObjectValueTask.ConfigureAwait(false); - BufferedProperty bufferedProperty = new BufferedProperty(); - bufferedProperty.PropertyNameNode = this.currentBufferedNode; + thisParam.currentBufferedNode = bufferedObject.ObjectStart; - (string propertyName, string annotationName) = await this.ReadPropertyNameAsync() - .ConfigureAwait(false); + await thisParam.ReadInternalAsync() + .ConfigureAwait(false); + } - bufferedProperty.PropertyAnnotationName = annotationName; - bufferedObject.AddBufferedProperty(propertyName, annotationName, bufferedProperty); + static async ValueTask AwaitReadInternalAsync( + ReorderingJsonReader thisParam, + Task pendingReadInternalTask) + { + await pendingReadInternalTask.ConfigureAwait(false); + } + } - if (annotationName != null) - { - // Instance-level property annotation - no reordering in its value - // or instance-level annotation - no reordering in its value either - // So skip its value while buffering. - await this.BufferValueAsync() - .ConfigureAwait(false); - } - } + /// + /// Processes an end object: finalizes the last property's value range, reorders properties, advances (if nested). + /// + /// true if this was the outermost object and processing is complete; otherwise false. + private ValueTask HandleEndObjectAsync(Stack bufferedObjectStack) + { + // End of object record + // Pop the node from our stack + BufferedObject bufferedObject = bufferedObjectStack.Pop(); - break; + // If there is a previous property record, mark its last value node. + MarkEndOfPropertyValueNode(bufferedObject, this.currentBufferedNode); - default: - // Read over (buffer) everything else - await this.ReadInternalAsync() - .ConfigureAwait(false); - break; + // Now perform the re-ordering on the buffered nodes + bufferedObject.Reorder(); + Debug.Assert( + this.currentBufferedNode.NodeType == JsonNodeType.EndObject, + "this.currentBufferedNode.NodeType == JsonNodeType.EndObject"); + + if (bufferedObjectStack.Count == 0) + { + // No more objects to process - we're done. + return ValueTask.FromResult(true); + } + + Task readInternalTask = this.ReadInternalAsync(); + if (readInternalTask.IsCompletedSuccessfully) + { + return ValueTask.FromResult(false); + } + + return AwaitReadInternalAsync(this, readInternalTask); + + static async ValueTask AwaitReadInternalAsync(ReorderingJsonReader thisParam, Task pendingReadInternalTask) + { + await pendingReadInternalTask.ConfigureAwait(false); + + return false; + } + } + + /// + /// Processes a property: closes the prior property's span, records the new property, + /// optionally buffers its annotation value. + /// + /// A ValueTask that completes once positioned after the property name + /// (or after the buffered annotation value). + private ValueTask HandlePropertyAsync(Stack bufferedObjectStack) + { + Debug.Assert(bufferedObjectStack.Count > 0, "bufferedObjectStack.Count > 0"); + + BufferedObject bufferedObject = bufferedObjectStack.Peek(); + + // If there is a current property, mark its last value node. + MarkEndOfPropertyValueNode(bufferedObject, this.currentBufferedNode); + + BufferedProperty bufferedProperty = new BufferedProperty { PropertyNameNode = this.currentBufferedNode }; + + ValueTask<(string PropertyName, string AnnotationName)> readPropertyNameTask = this.ReadPropertyNameAsync(); + if (readPropertyNameTask.IsCompletedSuccessfully) + { + (string propertyName, string annotationName) = readPropertyNameTask.Result; + + return ContinueAfterReadPropertyNameAsync( + this, + bufferedObject, + bufferedProperty, + propertyName, + annotationName); + } + + return AwaitReadPropertyNameAsync( + this, + bufferedObject, + bufferedProperty, + readPropertyNameTask); + + static ValueTask ContinueAfterReadPropertyNameAsync( + ReorderingJsonReader thisParam, + BufferedObject bufferedObjectParam, + BufferedProperty bufferedPropertyParam, + string propertyNameParam, + string annotationNameParam) + { + bufferedPropertyParam.PropertyAnnotationName = annotationNameParam; + bufferedObjectParam.AddBufferedProperty(propertyNameParam, annotationNameParam, bufferedPropertyParam); + + if (annotationNameParam != null) + { + // Instance-level property annotation - no reordering in its value + // or instance-level annotation - no reordering in its value either + // So skip its value while buffering. + + ValueTask bufferValueTask = thisParam.BufferValueAsync(); + if (!bufferValueTask.IsCompletedSuccessfully) + { + return bufferValueTask; // Caller will await + } } + + return ValueTask.CompletedTask; + } + + static async ValueTask AwaitReadPropertyNameAsync( + ReorderingJsonReader thisParam, + BufferedObject bufferedObjectParam, + BufferedProperty bufferedPropertyParam, + ValueTask<(string PropertyName, string AnnotationName)> pendingReadPropertyNameTask) + { + (string propertyName, string annotationName) = await pendingReadPropertyNameTask.ConfigureAwait(false); + + await ContinueAfterReadPropertyNameAsync( + thisParam, + bufferedObjectParam, + bufferedPropertyParam, + propertyName, + annotationName).ConfigureAwait(false); + } + } + + /// + /// Marks the end node of the current property's value using the node preceding the current cursor. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void MarkEndOfPropertyValueNode(BufferedObject bufferedObject, BufferedNode currentNode) + { + if (bufferedObject.CurrentProperty != null) + { + bufferedObject.CurrentProperty.EndOfPropertyValueNode = currentNode.Previous; } } @@ -389,22 +534,7 @@ private void BufferValue() int depth = 0; do { - switch (this.NodeType) - { - case JsonNodeType.PrimitiveValue: - break; - 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: - break; - } + depth = AdjustDepth(depth, this.NodeType); this.ReadInternal(); } @@ -419,54 +549,92 @@ private void BufferValue() /// 1). The name of the regular property which the reader is positioned on or which a property annotation belongs to. /// 2). The name of the instance or property annotation, or null if the reader is on a regular property. /// - private async Task<(string PropertyName, string AnnotationName)> ReadPropertyNameAsync() + private ValueTask<(string PropertyName, string AnnotationName)> ReadPropertyNameAsync() { - string propertyName, annotationName; - string jsonPropertyName = await this.GetPropertyNameAsync() - .ConfigureAwait(false); - Debug.Assert(!string.IsNullOrEmpty(jsonPropertyName), "The JSON reader guarantees that property names are not null or empty."); + Task getPropertyNameTask = this.GetPropertyNameAsync(); + if (getPropertyNameTask.IsCompletedSuccessfully) + { + string jsonPropertyName = getPropertyNameTask.Result; + Debug.Assert(!string.IsNullOrEmpty(jsonPropertyName), "The JSON reader guarantees that property names are not null or empty."); + Task readInternalTask = this.ReadInternalAsync(); + if (readInternalTask.IsCompletedSuccessfully) + { + ProcessProperty(jsonPropertyName, out string propertyName, out string annotationName); + return ValueTask.FromResult((propertyName, annotationName)); + } - await this.ReadInternalAsync() - .ConfigureAwait(false); + return AwaitReadInputAsync(this, jsonPropertyName, readInternalTask); + } - ProcessProperty(jsonPropertyName, out propertyName, out annotationName); + return AwaitGetPropertyNameAsync(this, getPropertyNameTask); + + static async ValueTask<(string PropertyName, string AnnotationName)> AwaitGetPropertyNameAsync( + ReorderingJsonReader thisParam, + Task pendingGetPropertyNameTask) + { + string jsonPropertyName = await pendingGetPropertyNameTask.ConfigureAwait(false); + + Debug.Assert(!string.IsNullOrEmpty(jsonPropertyName), "The JSON reader guarantees that property names are not null or empty."); + await thisParam.ReadInternalAsync() + .ConfigureAwait(false); - return (propertyName, annotationName); + ProcessProperty(jsonPropertyName, out string propertyName, out string annotationName); + return (propertyName, annotationName); + } + + static async ValueTask<(string PropertyName, string AnnotationName)> AwaitReadInputAsync( + ReorderingJsonReader thisParam, + string jsonPropertyName, + Task pendingReadInternalTask) + { + await pendingReadInternalTask.ConfigureAwait(false); + ProcessProperty(jsonPropertyName, out string propertyName, out string annotationName); + return (propertyName, annotationName); + } } /// /// Asynchronously reads over a value buffering it. /// /// A task that represents the asynchronous operation. - private async Task BufferValueAsync() + private ValueTask BufferValueAsync() { this.AssertBuffering(); // Skip the value buffering it in the process. int depth = 0; - do + + while (true) { - switch (this.NodeType) + depth = AdjustDepth(depth, this.NodeType); + + Task readInternalTask = this.ReadInternalAsync(); + if (!readInternalTask.IsCompletedSuccessfully) { - case JsonNodeType.PrimitiveValue: - break; - 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: - break; + return AwaitReadInputAsync(this, depth, readInternalTask); } - await this.ReadInternalAsync() - .ConfigureAwait(false); + if (depth == 0) + { + return ValueTask.CompletedTask; + } + } + + static async ValueTask AwaitReadInputAsync(ReorderingJsonReader thisParam, int depth, Task pendingReadInternalTask) + { + while (true) + { + await pendingReadInternalTask.ConfigureAwait(false); + + if (depth == 0) + { + return; + } + + depth = AdjustDepth(depth, thisParam.NodeType); + pendingReadInternalTask = thisParam.ReadInternalAsync(); + } } - while (depth > 0); } /// @@ -523,6 +691,35 @@ private static void ProcessProperty(string jsonPropertyName, out string property } } + /// + /// Adjusts the running nesting depth counter based on the current node type, + /// incrementing for start scopes and decrementing for end scopes. + /// + /// The current nesting depth. + /// The node just encountered. + /// The updated depth. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int AdjustDepth(int currentDepth, JsonNodeType nodeType) + { + switch (nodeType) + { + case JsonNodeType.PrimitiveValue: + return currentDepth; + + case JsonNodeType.StartArray: + case JsonNodeType.StartObject: + return currentDepth + 1; + + case JsonNodeType.EndArray: + case JsonNodeType.EndObject: + Debug.Assert(currentDepth > 0, "Seen too many scope ends."); + return currentDepth - 1; + + default: + return currentDepth; + } + } + /// /// A data structure to represent the buffered object with information about its properties, /// their order and annotations. diff --git a/test/UnitTests/Microsoft.OData.Core.Tests/Json/JsonReaderRegressionTests.cs b/test/UnitTests/Microsoft.OData.Core.Tests/Json/JsonReaderRegressionTests.cs index b0f0c71630..b82ac95bc3 100644 --- a/test/UnitTests/Microsoft.OData.Core.Tests/Json/JsonReaderRegressionTests.cs +++ b/test/UnitTests/Microsoft.OData.Core.Tests/Json/JsonReaderRegressionTests.cs @@ -4,11 +4,9 @@ // //--------------------------------------------------------------------- -using System; using System.Collections.Generic; using System.IO; using System.Text; -using System.Threading; using System.Threading.Tasks; using Microsoft.OData.Core; using Microsoft.OData.Json; @@ -33,6 +31,69 @@ public class JsonReaderRegressionTests "38752886587533208381420617177669147303598253490428755468731159562863882353787593751957781" + "8577805321712268066130019278766111959092164201989380952572010654858632788"; + private const string RichEntityPayload = "{\"@odata.context\":\"http://tempuri.org/$metadata#SuperEntities/$entity\"," + + "\"Id\":1," + + "\"BooleanProperty\":true," + + "\"Int32Property\":13," + + "\"SingleProperty\":3.142," + + "\"Int16Property\":7," + + "\"Int64Property\":6078747774547," + + "\"DoubleProperty\":3.14159265359," + + "\"DecimalProperty\":7654321," + + "\"GuidProperty\":\"00000017-003b-003b-0001-020304050607\"," + + "\"DateTimeOffsetProperty\":\"1970-12-31T23:59:59Z\"," + + "\"TimeSpanProperty\":\"PT23H59M59S\"," + + "\"ByteProperty\":1,\"SignedByteProperty\":9," + + "\"StringProperty\":\"foo\"," + + "\"DateProperty\":\"1970-01-01\"," + + "\"TimeOfDayProperty\":\"23:59:59.0000000\"," + + "\"ColorProperty\":\"Black\"," + + "\"GeographyPointProperty\":{\"type\":\"Point\",\"coordinates\":[22.2,22.2],\"crs\":{\"type\":\"name\",\"properties\":{\"name\":\"EPSG:4326\"}}}," + + "\"GeometryPointProperty\":{\"type\":\"Point\",\"coordinates\":[7.0,13.0],\"crs\":{\"type\":\"name\",\"properties\":{\"name\":\"EPSG:0\"}}}," + + "\"BooleanCollectionProperty\":[true,false]," + + "\"Int32CollectionProperty\":[13,31]," + + "\"SingleCollectionProperty\":[3.142,241.3]," + + "\"Int16CollectionProperty\":[7,11]," + + "\"Int64CollectionProperty\":[6078747774547,7454777478706]," + + "\"DoubleCollectionProperty\":[3.14159265359,95356295141.3]," + + "\"DecimalCollectionProperty\":[7654321,1234567]," + + "\"GuidCollectionProperty\":[\"00000017-003b-003b-0001-020304050607\",\"0000000b-001d-001d-0706-050403020100\"]," + + "\"DateTimeOffsetCollectionProperty\":[\"1970-12-31T23:59:59Z\",\"1858-11-17T11:29:29Z\"]," + + "\"TimeSpanCollectionProperty\":[\"PT23H59M59S\",\"PT11H29M29S\"]," + + "\"ByteCollectionProperty\":[1,9]," + + "\"SignedByteCollectionProperty\":[9,1]," + + "\"StringCollectionProperty\":[\"foo\",\"bar\"]," + + "\"DateCollectionProperty\":[\"1970-12-31\",\"1858-11-17\"]," + + "\"TimeOfDayCollectionProperty\":[\"23:59:59.0000000\",\"11:29:29.0000000\"]," + + "\"ColorCollectionProperty\":[\"Black\",\"White\"]," + + "\"GeographyPointCollectionProperty\":[" + + "{\"type\":\"Point\",\"coordinates\":[22.2,22.2],\"crs\":{\"type\":\"name\",\"properties\":{\"name\":\"EPSG:4326\"}}}," + + "{\"type\":\"Point\",\"coordinates\":[11.6,11.9],\"crs\":{\"type\":\"name\",\"properties\":{\"name\":\"EPSG:4326\"}}}]," + + "\"GeometryPointCollectionProperty\":[" + + "{\"type\":\"Point\",\"coordinates\":[7.0,13.0],\"crs\":{\"type\":\"name\",\"properties\":{\"name\":\"EPSG:0\"}}}," + + "{\"type\":\"Point\",\"coordinates\":[13.0,7.0],\"crs\":{\"type\":\"name\",\"properties\":{\"name\":\"EPSG:0\"}}}]," + + "\"DynamicPrimitiveProperty@odata.type\":\"#Edm.Double\"," + + "\"DynamicPrimitiveProperty\":3.14159265359," + + "\"DynamicSpatialProperty@odata.type\":\"#Edm.GeographyPoint\"," + + "\"DynamicSpatialProperty\":{\"type\":\"Point\",\"coordinates\":[11.1,11.1],\"crs\":{\"type\":\"name\",\"properties\":{\"name\":\"EPSG:4326\"}}}," + + "\"DynamicNullProperty@odata.type\":\"#Edm.String\"," + + "\"DynamicNullProperty\":null," + + "\"DynamicStringValueProperty@odata.type\":\"#Edm.String\"," + + "\"DynamicStringValueProperty\":\"The quick brown fox jumps over the lazy dog\"," + + "\"CoordinateProperty\":{\"Longitude\":47.64229583688,\"Latitude\":-122.13694393057}," + + "\"EntityProperty\":{\"@odata.id\":\"http://tempuri.org/Customers(1)\",\"Id\":1,\"Name\":\"Customer 1\"}," + + "\"CoordinateCollectionProperty\":[" + + "{\"Longitude\":47.64229583688,\"Latitude\":-122.13694393057}," + + "{\"Longitude\":-1.25873495895,\"Latitude\":36.80558172342}]," + + "\"EntityCollectionProperty\":[" + + "{\"@odata.id\":\"http://tempuri.org/Customers(1)\",\"Id\":1,\"Name\":\"Customer 1\"}," + + "{\"@odata.id\":\"http://tempuri.org/Customers(2)\",\"Id\":2,\"Name\":\"Customer 2\"}]," + + "\"DynamicComplexProperty\":{\"@odata.type\":\"#NS.Coordinate\",\"Longitude\":47.64229583688,\"Latitude\":-122.13694393057}," + + "\"DynamicComplexCollectionProperty@odata.type\":\"#Collection(NS.Coordinate)\"," + + "\"DynamicComplexCollectionProperty\":[" + + "{\"Longitude\":47.64229583688,\"Latitude\":-122.13694393057}," + + "{\"Longitude\":-1.25873495895,\"Latitude\":36.80558172342}]}"; + #endregion Constants public enum ReaderSourceKind @@ -152,6 +213,19 @@ private static TextReader CreateTextReader(string payload, ReaderSourceKind read public static IEnumerable PropertyBoundaryData_WithReaderKinds => ExpandWithReaderKinds(PropertyBoundaryData); + public static IEnumerable RichEntityPayloadData() + { + var whitespaceBeforeCommaPayload = RichEntityPayload.Replace("\"@odata.context\":", "\"@odata.context\"" + new string(' ', 2048) + ":"); + var whitespaceAfterCommaPayload = RichEntityPayload.Replace("\"@odata.context\":", "\"@odata.context\":" + new string(' ', 2048)); + + yield return new object[] { RichEntityPayload, ReaderSourceKind.Buffered }; + yield return new object[] { RichEntityPayload, ReaderSourceKind.Chunked }; + yield return new object[] { whitespaceBeforeCommaPayload, ReaderSourceKind.Buffered }; + yield return new object[] { whitespaceBeforeCommaPayload, ReaderSourceKind.Chunked }; + yield return new object[] { whitespaceAfterCommaPayload, ReaderSourceKind.Buffered }; + yield return new object[] { whitespaceAfterCommaPayload, ReaderSourceKind.Chunked }; + } + [Theory] [MemberData(nameof(NullLiteralBufferBoundaryData_WithReaderKinds))] public async Task Read_NullLiteral_BufferBoundaryAsync(string payload, ReaderSourceKind sourceKind) @@ -472,6 +546,467 @@ public async Task Read_Array_FirstElementAfterLargeWhitespace_ChunkedSourceAsync Assert.True(textReader.ObservedAsyncCompletion); } + [Theory] + [InlineData(ReaderSourceKind.Chunked)] + [InlineData(ReaderSourceKind.Buffered)] + public async Task Read_EscapedStringLiteralAsync(ReaderSourceKind sourceKind) + { + var escapedSegments = new[] + { + "\r", "\n", "\t", "\b", "\f", "\\", "\"", "\'", "\u00A0", "\uD834\uDD1E" + }; + + // Large payload helps us ensure we hit slow paths + var payload = "{" + BuildEscapedStringPropertiesPayload(16384, out int propertyCount) + "}"; + var textReader = CreateTextReader(payload, sourceKind, chunkSize: 128); + + using (var reader = new JsonReader(textReader, isIeee754Compatible: false)) + { + await reader.ReadAsync(); // { + int i = 0; + + while (i < propertyCount) + { + var escapedSegment = escapedSegments[i % 10]; + + await reader.ReadAsync(); // Property name + Assert.Equal($"Property{i + 1}", await reader.GetValueAsync()); + await reader.ReadAsync(); // Property value + var propertyValue = await reader.GetValueAsync(); + Assert.Equal($"String{escapedSegment}{i + 1}", await reader.GetValueAsync()); + i++; + } + } + + if (sourceKind == ReaderSourceKind.Chunked) + { + Assert.True(((ChunkedStringReader)textReader).ObservedAsyncCompletion); + } + } + + [Theory] + [MemberData(nameof(RichEntityPayloadData))] + public async Task Read_RichEntityPayloadAsync(string payload, ReaderSourceKind sourceKind) + { + var textReader = CreateTextReader(payload, sourceKind, chunkSize: 128); + + using (var reader = new JsonReader(textReader, isIeee754Compatible: false)) + { + await reader.ReadAsync(); // { + Assert.Equal(JsonNodeType.StartObject, reader.NodeType); + await ReadPropertyAsync(reader, "@odata.context", "http://tempuri.org/$metadata#SuperEntities/$entity"); + await ReadPropertyAsync(reader, "Id", 1); + await ReadPropertyAsync(reader, "BooleanProperty", true); + await ReadPropertyAsync(reader, "Int32Property", 13); + await ReadPropertyAsync(reader, "SingleProperty", 3.142m); // Read as decimal + await ReadPropertyAsync(reader, "Int16Property", 7); // Read as Int32 + await ReadPropertyAsync(reader, "Int64Property", 6078747774547m); // Read as decimal + await ReadPropertyAsync(reader, "DoubleProperty", 3.14159265359m); // Read as decimal + await ReadPropertyAsync(reader, "DecimalProperty", 7654321); // Read as Int32 + await ReadPropertyAsync(reader, "GuidProperty", "00000017-003b-003b-0001-020304050607"); // Read as string + await ReadPropertyAsync(reader, "DateTimeOffsetProperty", "1970-12-31T23:59:59Z"); // Read as string + await ReadPropertyAsync(reader, "TimeSpanProperty", "PT23H59M59S"); // Read as string + await ReadPropertyAsync(reader, "ByteProperty", 1); // Read as Int32 + await ReadPropertyAsync(reader, "SignedByteProperty", 9); // Read as Int32 + await ReadPropertyAsync(reader, "StringProperty", "foo"); + await ReadPropertyAsync(reader, "DateProperty", "1970-01-01"); // Read as string + await ReadPropertyAsync(reader, "TimeOfDayProperty", "23:59:59.0000000"); // Read as string + await ReadPropertyAsync(reader, "ColorProperty", "Black"); + await ReadPropertyNameAsync(reader, "GeographyPointProperty"); + await ReadSpatialAsync(reader, 22.2m, 22.2m, "EPSG:4326"); + await ReadPropertyNameAsync(reader, "GeometryPointProperty"); + await ReadSpatialAsync(reader, 7.0m, 13.0m, "EPSG:0"); + await ReadPropertyNameAsync(reader, "BooleanCollectionProperty"); + await reader.ReadAsync(); // [ + Assert.Equal(JsonNodeType.StartArray, reader.NodeType); + await ReadItemAsync(reader, true); // Item 1 + await ReadItemAsync(reader, false); // Item 2 + await reader.ReadAsync(); // ] + Assert.Equal(JsonNodeType.EndArray, reader.NodeType); + await ReadPropertyNameAsync(reader, "Int32CollectionProperty"); + await reader.ReadAsync(); // [ + Assert.Equal(JsonNodeType.StartArray, reader.NodeType); + await ReadItemAsync(reader, 13); // Item 1 + await ReadItemAsync(reader, 31); // Item 2 + await reader.ReadAsync(); // ] + Assert.Equal(JsonNodeType.EndArray, reader.NodeType); + await ReadPropertyNameAsync(reader, "SingleCollectionProperty"); + await reader.ReadAsync(); // [ + Assert.Equal(JsonNodeType.StartArray, reader.NodeType); + await ReadItemAsync(reader, 3.142m); // Item 1 + await ReadItemAsync(reader, 241.3m); // Item 2 + await reader.ReadAsync(); // ] + Assert.Equal(JsonNodeType.EndArray, reader.NodeType); + await ReadPropertyNameAsync(reader, "Int16CollectionProperty"); + await reader.ReadAsync(); // [ + Assert.Equal(JsonNodeType.StartArray, reader.NodeType); + await ReadItemAsync(reader, 7); // Item 1 + await ReadItemAsync(reader, 11); // Item 2 + await reader.ReadAsync(); // ] + Assert.Equal(JsonNodeType.EndArray, reader.NodeType); + await ReadPropertyNameAsync(reader, "Int64CollectionProperty"); + await reader.ReadAsync(); // [ + Assert.Equal(JsonNodeType.StartArray, reader.NodeType); + await ReadItemAsync(reader, 6078747774547m); // Item 1 + await ReadItemAsync(reader, 7454777478706m); // Item 2 + await reader.ReadAsync(); // ] + Assert.Equal(JsonNodeType.EndArray, reader.NodeType); + await ReadPropertyNameAsync(reader, "DoubleCollectionProperty"); + await reader.ReadAsync(); // [ + Assert.Equal(JsonNodeType.StartArray, reader.NodeType); + await ReadItemAsync(reader, 3.14159265359m); // Item 1 + await ReadItemAsync(reader, 95356295141.3m); // Item 2 + await reader.ReadAsync(); // ] + Assert.Equal(JsonNodeType.EndArray, reader.NodeType); + await ReadPropertyNameAsync(reader, "DecimalCollectionProperty"); + await reader.ReadAsync(); // [ + Assert.Equal(JsonNodeType.StartArray, reader.NodeType); + await ReadItemAsync(reader, 7654321); // Item 1 + await ReadItemAsync(reader, 1234567); // Item 2 + await reader.ReadAsync(); // ] + Assert.Equal(JsonNodeType.EndArray, reader.NodeType); + await ReadPropertyNameAsync(reader, "GuidCollectionProperty"); + await reader.ReadAsync(); // [ + Assert.Equal(JsonNodeType.StartArray, reader.NodeType); + await ReadItemAsync(reader, "00000017-003b-003b-0001-020304050607"); // Item 1 + await ReadItemAsync(reader, "0000000b-001d-001d-0706-050403020100"); // Item 2 + await reader.ReadAsync(); // ] + Assert.Equal(JsonNodeType.EndArray, reader.NodeType); + await ReadPropertyNameAsync(reader, "DateTimeOffsetCollectionProperty"); + await reader.ReadAsync(); // [ + Assert.Equal(JsonNodeType.StartArray, reader.NodeType); + await ReadItemAsync(reader, "1970-12-31T23:59:59Z"); // Item 1 + await ReadItemAsync(reader, "1858-11-17T11:29:29Z"); // Item 2 + await reader.ReadAsync(); // ] + Assert.Equal(JsonNodeType.EndArray, reader.NodeType); + await ReadPropertyNameAsync(reader, "TimeSpanCollectionProperty"); + await reader.ReadAsync(); // [ + Assert.Equal(JsonNodeType.StartArray, reader.NodeType); + await ReadItemAsync(reader, "PT23H59M59S"); // Item 1 + await ReadItemAsync(reader, "PT11H29M29S"); // Item 2 + await reader.ReadAsync(); // ] + Assert.Equal(JsonNodeType.EndArray, reader.NodeType); + await ReadPropertyNameAsync(reader, "ByteCollectionProperty"); + await reader.ReadAsync(); // [ + Assert.Equal(JsonNodeType.StartArray, reader.NodeType); + await ReadItemAsync(reader, 1); // Item 1 + await ReadItemAsync(reader, 9); // Item 2 + await reader.ReadAsync(); // ] + Assert.Equal(JsonNodeType.EndArray, reader.NodeType); + await ReadPropertyNameAsync(reader, "SignedByteCollectionProperty"); + await reader.ReadAsync(); // [ + Assert.Equal(JsonNodeType.StartArray, reader.NodeType); + await ReadItemAsync(reader, 9); // Item 1 + await ReadItemAsync(reader, 1); // Item 2 + await reader.ReadAsync(); // ] + Assert.Equal(JsonNodeType.EndArray, reader.NodeType); + await ReadPropertyNameAsync(reader, "StringCollectionProperty"); + await reader.ReadAsync(); // [ + Assert.Equal(JsonNodeType.StartArray, reader.NodeType); + await ReadItemAsync(reader, "foo"); // Item 1 + await ReadItemAsync(reader, "bar"); // Item 2 + await reader.ReadAsync(); // ] + Assert.Equal(JsonNodeType.EndArray, reader.NodeType); + await ReadPropertyNameAsync(reader, "DateCollectionProperty"); + await reader.ReadAsync(); // [ + Assert.Equal(JsonNodeType.StartArray, reader.NodeType); + await ReadItemAsync(reader, "1970-12-31"); // Item 1 + await ReadItemAsync(reader, "1858-11-17"); // Item 2 + await reader.ReadAsync(); // ] + Assert.Equal(JsonNodeType.EndArray, reader.NodeType); + await ReadPropertyNameAsync(reader, "TimeOfDayCollectionProperty"); + await reader.ReadAsync(); // [ + Assert.Equal(JsonNodeType.StartArray, reader.NodeType); + await ReadItemAsync(reader, "23:59:59.0000000"); // Item 1 + await ReadItemAsync(reader, "11:29:29.0000000"); // Item 2 + await reader.ReadAsync(); // ] + Assert.Equal(JsonNodeType.EndArray, reader.NodeType); + await ReadPropertyNameAsync(reader, "ColorCollectionProperty"); + await reader.ReadAsync(); // [ + Assert.Equal(JsonNodeType.StartArray, reader.NodeType); + await ReadItemAsync(reader, "Black"); // Item 1 + await ReadItemAsync(reader, "White"); // Item 2 + await reader.ReadAsync(); // ] + Assert.Equal(JsonNodeType.EndArray, reader.NodeType); + await ReadPropertyNameAsync(reader, "GeographyPointCollectionProperty"); + await reader.ReadAsync(); // [ + Assert.Equal(JsonNodeType.StartArray, reader.NodeType); + await ReadSpatialAsync(reader, 22.2m, 22.2m, "EPSG:4326"); + await ReadSpatialAsync(reader, 11.6m, 11.9m, "EPSG:4326"); + await reader.ReadAsync(); // ] + Assert.Equal(JsonNodeType.EndArray, reader.NodeType); + await ReadPropertyNameAsync(reader, "GeometryPointCollectionProperty"); + await reader.ReadAsync(); // [ + Assert.Equal(JsonNodeType.StartArray, reader.NodeType); + await ReadSpatialAsync(reader, 7.0m, 13.0m, "EPSG:0"); + await ReadSpatialAsync(reader, 13.0m, 7.0m, "EPSG:0"); + await reader.ReadAsync(); // ] + Assert.Equal(JsonNodeType.EndArray, reader.NodeType); + await ReadPropertyAsync(reader, "DynamicPrimitiveProperty@odata.type", "#Edm.Double"); + await ReadPropertyAsync(reader, "DynamicPrimitiveProperty", 3.14159265359m); + await ReadPropertyAsync(reader, "DynamicSpatialProperty@odata.type", "#Edm.GeographyPoint"); + await ReadPropertyNameAsync(reader, "DynamicSpatialProperty"); + await ReadSpatialAsync(reader, 11.1m, 11.1m, "EPSG:4326"); + await ReadPropertyAsync(reader, "DynamicNullProperty@odata.type", "#Edm.String"); + await ReadPropertyAsync(reader, "DynamicNullProperty", null); + await ReadPropertyAsync(reader, "DynamicStringValueProperty@odata.type", "#Edm.String"); + await ReadPropertyAsync(reader, "DynamicStringValueProperty", "The quick brown fox jumps over the lazy dog"); + await ReadPropertyNameAsync(reader, "CoordinateProperty"); + await ReadComplexAsync(reader, 47.64229583688m, -122.13694393057m); + await ReadPropertyNameAsync(reader, "EntityProperty"); + await AssertEntityAsync(reader, "http://tempuri.org/Customers(1)", 1, "Customer 1"); + await ReadPropertyNameAsync(reader, "CoordinateCollectionProperty"); + await reader.ReadAsync(); // [ + Assert.Equal(JsonNodeType.StartArray, reader.NodeType); + await ReadComplexAsync(reader, 47.64229583688m, -122.13694393057m); + await ReadComplexAsync(reader, -1.25873495895m, 36.80558172342m); + await reader.ReadAsync(); // ] + Assert.Equal(JsonNodeType.EndArray, reader.NodeType); + await ReadPropertyNameAsync(reader, "EntityCollectionProperty"); + await reader.ReadAsync(); // [ + Assert.Equal(JsonNodeType.StartArray, reader.NodeType); + await AssertEntityAsync(reader, "http://tempuri.org/Customers(1)", 1, "Customer 1"); + await AssertEntityAsync(reader, "http://tempuri.org/Customers(2)", 2, "Customer 2"); + await reader.ReadAsync(); // ] + Assert.Equal(JsonNodeType.EndArray, reader.NodeType); + await ReadPropertyNameAsync(reader, "DynamicComplexProperty"); + await reader.ReadAsync(); // { + Assert.Equal(JsonNodeType.StartObject, reader.NodeType); + await ReadPropertyAsync(reader, "@odata.type", "#NS.Coordinate"); + await ReadPropertyAsync(reader, "Longitude", 47.64229583688m); + await ReadPropertyAsync(reader, "Latitude", -122.13694393057m); + await reader.ReadAsync(); // } + Assert.Equal(JsonNodeType.EndObject, reader.NodeType); + await ReadPropertyAsync(reader, "DynamicComplexCollectionProperty@odata.type", "#Collection(NS.Coordinate)"); + await ReadPropertyNameAsync(reader, "DynamicComplexCollectionProperty"); + await reader.ReadAsync(); // [ + Assert.Equal(JsonNodeType.StartArray, reader.NodeType); + await ReadComplexAsync(reader, 47.64229583688m, -122.13694393057m); + await ReadComplexAsync(reader, -1.25873495895m, 36.80558172342m); + await reader.ReadAsync(); // ] + Assert.Equal(JsonNodeType.EndArray, reader.NodeType); + await reader.ReadAsync(); // } + Assert.Equal(JsonNodeType.EndObject, reader.NodeType); + } + + if (sourceKind == ReaderSourceKind.Chunked) + { + Assert.True(((ChunkedStringReader)textReader).ObservedAsyncCompletion); + } + + static async ValueTask ReadPropertyNameAsync(JsonReader readerParam, string propertyName) + { + await readerParam.ReadAsync(); // Property name + Assert.Equal(propertyName, await readerParam.GetValueAsync()); + } + + static async ValueTask ReadPropertyAsync(JsonReader readerParam, string propertyName, object propertyValue) + { + await ReadPropertyNameAsync(readerParam, propertyName); + await readerParam.ReadAsync(); // Property value + Assert.Equal(propertyValue, await readerParam.GetValueAsync()); + } + + static async ValueTask ReadItemAsync(JsonReader readerParam, object itemValue) + { + await readerParam.ReadAsync(); // Item + Assert.Equal(itemValue, await readerParam.GetValueAsync()); + } + + static async ValueTask ReadSpatialAsync(JsonReader readerParam, decimal longitudeOrX, decimal latitudeOrX, string epsg) + { + await readerParam.ReadAsync(); // { + Assert.Equal(JsonNodeType.StartObject, readerParam.NodeType); + await ReadPropertyAsync(readerParam, "type", "Point"); + await ReadPropertyNameAsync(readerParam, "coordinates"); + await readerParam.ReadAsync(); // [ + Assert.Equal(JsonNodeType.StartArray, readerParam.NodeType); + await ReadItemAsync(readerParam, longitudeOrX); // Longitude or X + await ReadItemAsync(readerParam, latitudeOrX); // Latitude or Y + await readerParam.ReadAsync(); // ] + Assert.Equal(JsonNodeType.EndArray, readerParam.NodeType); + await ReadPropertyNameAsync(readerParam, "crs"); + await readerParam.ReadAsync(); // { + Assert.Equal(JsonNodeType.StartObject, readerParam.NodeType); + await ReadPropertyAsync(readerParam, "type", "name"); + await ReadPropertyNameAsync(readerParam, "properties"); + await readerParam.ReadAsync(); // { + Assert.Equal(JsonNodeType.StartObject, readerParam.NodeType); + await ReadPropertyAsync(readerParam, "name", epsg); + await readerParam.ReadAsync(); // } + Assert.Equal(JsonNodeType.EndObject, readerParam.NodeType); + await readerParam.ReadAsync(); // } + Assert.Equal(JsonNodeType.EndObject, readerParam.NodeType); + await readerParam.ReadAsync(); // } + Assert.Equal(JsonNodeType.EndObject, readerParam.NodeType); + } + + static async ValueTask ReadComplexAsync(JsonReader readerParam, decimal longitude, decimal latitude) + { + await readerParam.ReadAsync(); // { + Assert.Equal(JsonNodeType.StartObject, readerParam.NodeType); + await ReadPropertyAsync(readerParam, "Longitude", longitude); + await ReadPropertyAsync(readerParam, "Latitude", latitude); + await readerParam.ReadAsync(); // } + Assert.Equal(JsonNodeType.EndObject, readerParam.NodeType); + } + + static async ValueTask AssertEntityAsync(JsonReader readerParam, string odataId, int id, string name) + { + await readerParam.ReadAsync(); // { + Assert.Equal(JsonNodeType.StartObject, readerParam.NodeType); + await ReadPropertyAsync(readerParam, "@odata.id", odataId); + await ReadPropertyAsync(readerParam, "Id", id); + await ReadPropertyAsync(readerParam, "Name", name); + await readerParam.ReadAsync(); // } + Assert.Equal(JsonNodeType.EndObject, readerParam.NodeType); + } + } + + [Theory] + [InlineData(ReaderSourceKind.Chunked, "\\", "\\1")] + [InlineData(ReaderSourceKind.Chunked, "\\\"\\", "\\1")] + [InlineData(ReaderSourceKind.Chunked, "\\'\\", "\\1")] + [InlineData(ReaderSourceKind.Chunked, "\\u", "\\u149\"")] + [InlineData(ReaderSourceKind.Buffered, "\\", "\\1")] + [InlineData(ReaderSourceKind.Buffered, "\\\"\\", "\\1")] + [InlineData(ReaderSourceKind.Buffered, "\\'\\", "\\1")] + [InlineData(ReaderSourceKind.Buffered, "\\u", "\\u149\"")] + public async Task Read_IncompleteEscapeSequence_ThrowsAsync(ReaderSourceKind sourceKind, string sequence, string fragment) + { + var escapedSegments = new[] + { + "\r", "\n", "\t", "\b", "\f", "\\", "\"", "\'", "\u00A0", "\uD834\uDD1E" + }; + + // Large payload helps us ensure we hit slow paths in escape sequence handling + var escapedProperties = BuildEscapedStringPropertiesPayload(4096, out int propertyCount); + var nextProperty = propertyCount + 1; + escapedProperties += $",\"Property{nextProperty}\":\"String{sequence}{nextProperty}\""; + var payload = "{" + escapedProperties + "}"; + var textReader = CreateTextReader(payload, sourceKind, chunkSize: 128); + + using (var reader = new JsonReader(textReader, isIeee754Compatible: false)) + { + await reader.ReadAsync(); // { + int i = 0; + + while (i < propertyCount) + { + var escapedSegment = escapedSegments[i % 10]; + + await reader.ReadAsync(); // Property name + Assert.Equal($"Property{i + 1}", await reader.GetValueAsync()); + await reader.ReadAsync(); // Property value + var propertyValue = await reader.GetValueAsync(); + Assert.Equal($"String{escapedSegment}{i + 1}", await reader.GetValueAsync()); + i++; + } + + // Read property with incomplete escape sequence + await reader.ReadAsync(); // Property name + Assert.Equal($"Property{nextProperty}", await reader.GetValueAsync()); + await reader.ReadAsync(); // Property value + var exception = await Assert.ThrowsAsync(reader.GetValueAsync); + + Assert.Equal( + $"Invalid JSON. An unrecognized escape sequence '{fragment}' was found in a JSON string value.", + exception.Message); + } + + if (sourceKind == ReaderSourceKind.Chunked) + { + Assert.True(((ChunkedStringReader)textReader).ObservedAsyncCompletion); + } + } + + [Theory] + [InlineData(ReaderSourceKind.Chunked, "\\", "\\")] + [InlineData(ReaderSourceKind.Chunked, "\\\"\\", "\\")] + [InlineData(ReaderSourceKind.Chunked, "\\'\\", "\\")] + [InlineData(ReaderSourceKind.Chunked, "\\u", "\\u")] + [InlineData(ReaderSourceKind.Chunked, "\\u00", "\\u00")] + [InlineData(ReaderSourceKind.Buffered, "\\", "\\")] + [InlineData(ReaderSourceKind.Buffered, "\\\"\\", "\\")] + [InlineData(ReaderSourceKind.Buffered, "\\'\\", "\\")] + [InlineData(ReaderSourceKind.Buffered, "\\u", "\\u")] + [InlineData(ReaderSourceKind.Buffered, "\\u00", "\\u00")] + public async Task Read_EofAtEndOfEscapeSequence_ThrowsAsync(ReaderSourceKind sourceKind, string sequence, string fragment) + { + var escapedSegments = new[] + { + "\r", "\n", "\t", "\b", "\f", "\\", "\"", "\'", "\u00A0", "\uD834\uDD1E" + }; + + // Large payload helps us ensure we hit slow paths in escape sequence handling + var escapedProperties = BuildEscapedStringPropertiesPayload(4096, out int propertyCount); + var nextProperty = propertyCount + 1; + escapedProperties += $",\"Property{nextProperty}\":\"String{sequence}"; + var payload = "{" + escapedProperties; + var textReader = CreateTextReader(payload, sourceKind, chunkSize: 128); + + using (var reader = new JsonReader(textReader, isIeee754Compatible: false)) + { + await reader.ReadAsync(); // { + int i = 0; + + while (i < propertyCount) + { + var escapedSegment = escapedSegments[i % 10]; + + await reader.ReadAsync(); // Property name + Assert.Equal($"Property{i + 1}", await reader.GetValueAsync()); + await reader.ReadAsync(); // Property value + var propertyValue = await reader.GetValueAsync(); + Assert.Equal($"String{escapedSegment}{i + 1}", await reader.GetValueAsync()); + i++; + } + + // Read property with incomplete escape sequence + await reader.ReadAsync(); // Property name + Assert.Equal($"Property{nextProperty}", await reader.GetValueAsync()); + await reader.ReadAsync(); // Property value + var exception = await Assert.ThrowsAsync(reader.GetValueAsync); + + Assert.Equal( + $"Invalid JSON. An unrecognized escape sequence '{fragment}' was found in a JSON string value.", + exception.Message); + } + + if (sourceKind == ReaderSourceKind.Chunked) + { + Assert.True(((ChunkedStringReader)textReader).ObservedAsyncCompletion); + } + } + + [Theory] + [InlineData(ReaderSourceKind.Chunked)] + [InlineData(ReaderSourceKind.Buffered)] + public async Task Read_EofAtEndOfLongUnterminatedStringLiteral_ThrowsAsync(ReaderSourceKind sourceKind) + { + // Large payload helps us ensure we hit slow paths + var payload = "{\"Property\":\"" + new string('s', 2048); + var textReader = CreateTextReader(payload, sourceKind, chunkSize: 128); + + using (var reader = new JsonReader(textReader, isIeee754Compatible: false)) + { + await reader.ReadAsync(); // { + await reader.ReadAsync(); // Property name + Assert.Equal($"Property", await reader.GetValueAsync()); + await reader.ReadAsync(); // Property value + var exception = await Assert.ThrowsAsync(reader.GetValueAsync); + + Assert.Equal( + "Invalid JSON. Unexpected end of input reached while processing a JSON string value.", + exception.Message); + } + + if (sourceKind == ReaderSourceKind.Chunked) + { + Assert.True(((ChunkedStringReader)textReader).ObservedAsyncCompletion); + } + } + [Fact] public async Task Read_PropertyNameQuoted_MissingColon_ChunkedSource_ThrowsAsync() { @@ -667,5 +1202,69 @@ static string BuildStringPropertiesPayload(int lengthHint, out int propertyCount return builder.ToString(); } + + static string BuildEscapedStringPropertiesPayload(int lengthHint, out int propertyCount) + { + // Cycle of 10 escape kinds (index % 10): + // 1:\r 2:\n 3:\t 4:\b 5:\f 6:\\ 7:\" 8:\' 9:\u00A0 0: \uD834\uDD1E (surrogate pair) + var builder = new StringBuilder(lengthHint + 64); + int i = 1; + bool first = true; + + while (builder.Length < lengthHint) + { + if (!first) + { + builder.Append(','); + } + first = false; + + builder.Append("\"Property"); + builder.Append(i); + builder.Append("\":\"String"); + + int mod = i % 10; + switch (mod) + { + case 1: + builder.Append("\\r"); // carriage return + break; + case 2: + builder.Append("\\n"); // newline + break; + case 3: + builder.Append("\\t"); // tab + break; + case 4: + builder.Append("\\b"); // backspace + break; + case 5: + builder.Append("\\f"); // form-feed + break; + case 6: + builder.Append("\\\\"); // backslash + break; + case 7: + builder.Append("\\\""); // double quote + break; + case 8: + builder.Append("\\\'"); // single quote + break; + case 9: + builder.Append("\\u00A0"); + break; + case 0: + builder.Append("\\uD834\\uDD1E"); + break; + } + + builder.Append(i); + builder.Append('"'); + i++; + } + + propertyCount = i - 1; + return builder.ToString(); + } } } diff --git a/test/UnitTests/Microsoft.OData.Core.Tests/Json/JsonReaderTests.cs b/test/UnitTests/Microsoft.OData.Core.Tests/Json/JsonReaderTests.cs index 542e6281c4..f7e7680b2a 100644 --- a/test/UnitTests/Microsoft.OData.Core.Tests/Json/JsonReaderTests.cs +++ b/test/UnitTests/Microsoft.OData.Core.Tests/Json/JsonReaderTests.cs @@ -651,7 +651,7 @@ public async Task UnexpectedNullTokenThrowsException() [Theory] [InlineData("{\"Data\":\"The \\ r character\"}", "\\ ")] [InlineData("{\"Data\":\"The \\", "\\")] - [InlineData("{\"Data\":\"The \\u621", "\\uXXXX")] + [InlineData("{\"Data\":\"The \\u621", "\\u621")] [InlineData("{\"Data\":\"The \\u62 character\"}", "\\u62 c")] public async Task UnrecognizedEscapeSequenceThrowsException(string payload, string expected) {