diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/BitStack.cs b/src/libraries/System.Text.Json/src/System/Text/Json/BitStack.cs index 40d70a56a5c4bf..e96622e16952d6 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/BitStack.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/BitStack.cs @@ -14,6 +14,7 @@ internal struct BitStack private const int DefaultInitialArraySize = 2; + // The backing array for the stack used when the depth exceeds AllocationFreeMaxDepth. private int[]? _array; // This ulong container represents a tiny stack to track the state during nested transitions. @@ -26,8 +27,14 @@ internal struct BitStack private int _currentDepth; - public int CurrentDepth => _currentDepth; + /// + /// Gets the number of elements in the stack. + /// + public readonly int CurrentDepth => _currentDepth; + /// + /// Pushes onto the stack. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void PushTrue() { @@ -42,6 +49,9 @@ public void PushTrue() _currentDepth++; } + /// + /// Pushes onto the stack. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void PushFalse() { @@ -56,7 +66,10 @@ public void PushFalse() _currentDepth++; } - // Allocate the bit array lazily only when it is absolutely necessary + /// + /// Pushes a bit onto the stack. Allocate the bit array lazily only when it is absolutely necessary. + /// + /// The bit to push onto the stack. [MethodImpl(MethodImplOptions.NoInlining)] private void PushToArray(bool value) { @@ -94,6 +107,10 @@ private void PushToArray(bool value) _array[elementIndex] = newValue; } + /// + /// Pops the bit at the top of the stack and returns its value. + /// + /// The bit that was popped. [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Pop() { @@ -110,13 +127,18 @@ public bool Pop() } else { - inObject = PopFromArray(); + // Decrementing depth above effectively pops the last element in the array-backed case. + inObject = PeekInArray(); } return inObject; } + /// + /// If the stack has a backing array allocated, this method will find the topmost bit in the array and return its value. + /// This should only be called if the depth is greater than AllocationFreeMaxDepth and an array has been allocated. + /// [MethodImpl(MethodImplOptions.NoInlining)] - private bool PopFromArray() + private readonly bool PeekInArray() { int index = _currentDepth - AllocationFreeMaxDepth - 1; Debug.Assert(_array != null); @@ -129,6 +151,14 @@ private bool PopFromArray() return (_array[elementIndex] & (1 << extraBits)) != 0; } + /// + /// Peeks at the bit at the top of the stack. + /// + /// The bit at the top of the stack. + public readonly bool Peek() + // If the stack is small enough, we can use the allocation-free container, otherwise check the allocated array. + => _currentDepth <= AllocationFreeMaxDepth ? (_allocationFreeContainer & 1) != 0 : PeekInArray(); + private void DoubleArray(int minSize) { Debug.Assert(_array != null); @@ -141,6 +171,9 @@ private void DoubleArray(int minSize) Array.Resize(ref _array, nextDouble); } + /// + /// Optimization to push as the first bit when the stack is empty. + /// public void SetFirstBit() { Debug.Assert(_currentDepth == 0, "Only call SetFirstBit when depth is 0"); @@ -148,6 +181,9 @@ public void SetFirstBit() _allocationFreeContainer = 1; } + /// + /// Optimization to push as the first bit when the stack is empty. + /// public void ResetFirstBit() { Debug.Assert(_currentDepth == 0, "Only call ResetFirstBit when depth is 0"); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/JsonTokenType.cs b/src/libraries/System.Text.Json/src/System/Text/Json/JsonTokenType.cs index 049da2220a22f8..be2d8fcfdcbf76 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/JsonTokenType.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/JsonTokenType.cs @@ -12,8 +12,8 @@ namespace System.Text.Json /// public enum JsonTokenType : byte { - // Do not re-order. - // We rely on the ordering to quickly check things like IsTokenTypePrimitive + // Do not re-number. + // We rely on the underlying values to quickly check things like JsonReaderHelper.IsTokenTypePrimitive and Utf8JsonWriter.CanWriteValue /// /// Indicates that there is no value (as distinct from ). @@ -21,61 +21,61 @@ public enum JsonTokenType : byte /// /// This is the default token type if no data has been read by the . /// - None, + None = 0, /// /// Indicates that the token type is the start of a JSON object. /// - StartObject, + StartObject = 1, /// /// Indicates that the token type is the end of a JSON object. /// - EndObject, + EndObject = 2, /// /// Indicates that the token type is the start of a JSON array. /// - StartArray, + StartArray = 3, /// /// Indicates that the token type is the end of a JSON array. /// - EndArray, + EndArray = 4, /// /// Indicates that the token type is a JSON property name. /// - PropertyName, + PropertyName = 5, /// /// Indicates that the token type is the comment string. /// - Comment, + Comment = 6, /// /// Indicates that the token type is a JSON string. /// - String, + String = 7, /// /// Indicates that the token type is a JSON number. /// - Number, + Number = 8, /// /// Indicates that the token type is the JSON literal true. /// - True, + True = 9, /// /// Indicates that the token type is the JSON literal false. /// - False, + False = 10, /// /// Indicates that the token type is the JSON literal null. /// - Null, + Null = 11, } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.cs b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.cs index 2bc50fcfeb4d39..e6a7790d2b757b 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; +using static System.Text.Json.Utf8JsonWriter; namespace System.Text.Json { @@ -312,9 +313,23 @@ public static void ThrowInvalidOperationException_CannotSkipOnPartial() } [DoesNotReturn] - public static void ThrowInvalidOperationException_CannotMixEncodings(Utf8JsonWriter.SegmentEncoding previousEncoding, Utf8JsonWriter.SegmentEncoding currentEncoding) + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowInvalidOperationException_CannotMixEncodings(EnclosingContainerType previousEncoding, EnclosingContainerType currentEncoding) { - throw GetInvalidOperationException(SR.Format(SR.CannotMixEncodings, previousEncoding, currentEncoding)); + throw GetInvalidOperationException(SR.Format(SR.CannotMixEncodings, GetEncodingName(previousEncoding), GetEncodingName(currentEncoding))); + + static string GetEncodingName(EnclosingContainerType encoding) + { + switch (encoding) + { + case EnclosingContainerType.Utf8StringSequence: return "UTF-8"; + case EnclosingContainerType.Utf16StringSequence: return "UTF-16"; + case EnclosingContainerType.Base64StringSequence: return "Base64"; + default: + Debug.Fail("Unknown encoding."); + return "Unknown"; + }; + } } private static InvalidOperationException GetInvalidOperationException(string message, JsonTokenType tokenType) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Helpers.cs index b3b85281b82cc9..808255ae2a9687 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Helpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Helpers.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -36,13 +37,10 @@ private void ValidateWritingProperty() { if (!_options.SkipValidation) { - // Make sure a new property is not attempted within an unfinalized string. - ValidateNotWithinUnfinalizedString(); - - if (!_inObject || _tokenType == JsonTokenType.PropertyName) + if (_enclosingContainer != EnclosingContainerType.Object || _tokenType == JsonTokenType.PropertyName) { Debug.Assert(_tokenType != JsonTokenType.StartObject); - ThrowHelper.ThrowInvalidOperationException(ExceptionResource.CannotWritePropertyWithinArray, currentDepth: default, maxDepth: _options.MaxDepth, token: default, _tokenType); + OnValidateWritingPropertyFailed(); } } } @@ -52,18 +50,28 @@ private void ValidateWritingProperty(byte token) { if (!_options.SkipValidation) { - // Make sure a new property is not attempted within an unfinalized string. - ValidateNotWithinUnfinalizedString(); - - if (!_inObject || _tokenType == JsonTokenType.PropertyName) + if (_enclosingContainer != EnclosingContainerType.Object || _tokenType == JsonTokenType.PropertyName) { Debug.Assert(_tokenType != JsonTokenType.StartObject); - ThrowHelper.ThrowInvalidOperationException(ExceptionResource.CannotWritePropertyWithinArray, currentDepth: default, maxDepth: _options.MaxDepth, token: default, _tokenType); + OnValidateWritingPropertyFailed(); } UpdateBitStackOnStart(token); } } + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + private void OnValidateWritingPropertyFailed() + { + if (IsWritingPartialString) + { + ThrowInvalidOperationException(ExceptionResource.CannotWriteWithinString); + } + + Debug.Assert(_enclosingContainer != EnclosingContainerType.Object || _tokenType == JsonTokenType.PropertyName); + ThrowInvalidOperationException(ExceptionResource.CannotWritePropertyWithinArray); + } + private void WritePropertyNameMinimized(ReadOnlySpan escapedPropertyName, byte token) { Debug.Assert(escapedPropertyName.Length < int.MaxValue - 5); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Helpers.cs index 00f927aae9c791..1a450b476919c0 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Helpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Helpers.cs @@ -12,54 +12,116 @@ namespace System.Text.Json { public sealed partial class Utf8JsonWriter { - private bool HasPartialStringData => PartialStringDataLength != 0; + /// + /// Returns whether a JSON value can be written at the current position based on the current : + /// + /// + /// : Writing a value is always allowed. + /// + /// + /// : Writing a value is allowed only if is a property name. + /// Because we designed == , we can just check for equality. + /// + /// + /// : Writing a value is allowed only if is None (only one value may be written at the root). + /// This case is identical to the previous case. + /// + /// + /// , , : + /// Writing a value is never valid and does not equal any by construction. + /// + /// + /// This method performs better without short circuiting (this often gets inlined so using simple branch free code seems to have some benefits). + /// + private bool CanWriteValue => _enclosingContainer == EnclosingContainerType.Array | (byte)_enclosingContainer == (byte)_tokenType; - private void ClearPartialStringData() => PartialStringDataLength = 0; + private bool HasPartialStringData => _partialStringDataLength != 0; - private void ValidateEncodingDidNotChange(SegmentEncoding currentSegmentEncoding) + private void ClearPartialStringData() => _partialStringDataLength = 0; + + private void ValidateWritingValue() { - if (PreviousSegmentEncoding != currentSegmentEncoding) + if (!CanWriteValue) { - ThrowHelper.ThrowInvalidOperationException_CannotMixEncodings(PreviousSegmentEncoding, currentSegmentEncoding); + OnValidateWritingValueFailed(); } } - private void ValidateNotWithinUnfinalizedString() + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + private void OnValidateWritingValueFailed() { - if (_tokenType == StringSegmentSentinel) + Debug.Assert(!_options.SkipValidation); + + if (IsWritingPartialString) { - ThrowHelper.ThrowInvalidOperationException(ExceptionResource.CannotWriteWithinString, currentDepth: default, maxDepth: _options.MaxDepth, token: default, _tokenType); + ThrowInvalidOperationException(ExceptionResource.CannotWriteWithinString); } - Debug.Assert(PreviousSegmentEncoding == SegmentEncoding.None); Debug.Assert(!HasPartialStringData); + + if (_enclosingContainer == EnclosingContainerType.Object) + { + Debug.Assert(_tokenType != JsonTokenType.PropertyName); + Debug.Assert(_tokenType != JsonTokenType.None && _tokenType != JsonTokenType.StartArray); + ThrowInvalidOperationException(ExceptionResource.CannotWriteValueWithinObject); + } + else + { + Debug.Assert(_tokenType != JsonTokenType.PropertyName); + Debug.Assert(CurrentDepth == 0 && _tokenType != JsonTokenType.None); + ThrowInvalidOperationException(ExceptionResource.CannotWriteValueAfterPrimitiveOrClose); + } } - private void ValidateWritingValue() + private void ValidateWritingSegment(EnclosingContainerType currentSegmentEncoding) { - Debug.Assert(!_options.SkipValidation); + Debug.Assert(currentSegmentEncoding is EnclosingContainerType.Utf8StringSequence or EnclosingContainerType.Utf16StringSequence or EnclosingContainerType.Base64StringSequence); + + // A string segment can be written if either: + // 1) The writer is currently in a partial string of the same type. In this case the new segment + // will continue the partial string. + // - or - + // 2) The writer can write a value at the current position, in which case a new string can be started. + if (_enclosingContainer != currentSegmentEncoding && !CanWriteValue) + { + OnValidateWritingSegmentFailed(currentSegmentEncoding); + } + } + + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + private void OnValidateWritingSegmentFailed(EnclosingContainerType currentSegmentEncoding) + { + if (IsWritingPartialString) + { + ThrowHelper.ThrowInvalidOperationException_CannotMixEncodings(_enclosingContainer, currentSegmentEncoding); + } - // Make sure a new value is not attempted within an unfinalized string. - ValidateNotWithinUnfinalizedString(); + Debug.Assert(!HasPartialStringData); - if (_inObject) + if (_enclosingContainer == EnclosingContainerType.Object) { - if (_tokenType != JsonTokenType.PropertyName) - { - Debug.Assert(_tokenType != JsonTokenType.None && _tokenType != JsonTokenType.StartArray); - ThrowHelper.ThrowInvalidOperationException(ExceptionResource.CannotWriteValueWithinObject, currentDepth: default, maxDepth: _options.MaxDepth, token: default, _tokenType); - } + Debug.Assert(_tokenType != JsonTokenType.PropertyName); + Debug.Assert(_tokenType != JsonTokenType.None && _tokenType != JsonTokenType.StartArray); + ThrowInvalidOperationException(ExceptionResource.CannotWriteValueWithinObject); } else { Debug.Assert(_tokenType != JsonTokenType.PropertyName); + Debug.Assert(CurrentDepth == 0 && _tokenType != JsonTokenType.None); + ThrowInvalidOperationException(ExceptionResource.CannotWriteValueAfterPrimitiveOrClose); + } + } - // It is more likely for CurrentDepth to not equal 0 when writing valid JSON, so check that first to rely on short-circuiting and return quickly. - if (CurrentDepth == 0 && _tokenType != JsonTokenType.None) - { - ThrowHelper.ThrowInvalidOperationException(ExceptionResource.CannotWriteValueAfterPrimitiveOrClose, currentDepth: default, maxDepth: _options.MaxDepth, token: default, _tokenType); - } + private void ValidateNotWithinUnfinalizedString() + { + if (IsWritingPartialString) + { + ThrowInvalidOperationException(ExceptionResource.CannotWriteWithinString); } + + Debug.Assert(!HasPartialStringData); } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.StringSegment.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.StringSegment.cs index 97efe65aba655c..1016a9f76ff25e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.StringSegment.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.StringSegment.cs @@ -28,24 +28,15 @@ public void WriteStringValueSegment(ReadOnlySpan value, bool isFinalSegmen { JsonWriterHelper.ValidateValue(value); - if (_tokenType != Utf8JsonWriter.StringSegmentSentinel) + if (!_options.SkipValidation) { - Debug.Assert(PreviousSegmentEncoding == SegmentEncoding.None); - Debug.Assert(!HasPartialStringData); - - if (!_options.SkipValidation) - { - ValidateWritingValue(); - } - - WriteStringSegmentPrologue(); - - PreviousSegmentEncoding = SegmentEncoding.Utf16; - _tokenType = Utf8JsonWriter.StringSegmentSentinel; + ValidateWritingSegment(EnclosingContainerType.Utf16StringSequence); } - else + + if (_enclosingContainer != EnclosingContainerType.Utf16StringSequence) { - ValidateEncodingDidNotChange(SegmentEncoding.Utf16); + WriteStringSegmentPrologue(); + _enclosingContainer = EnclosingContainerType.Utf16StringSequence; } // The steps to write a string segment are to complete the previous partial code point @@ -64,7 +55,8 @@ public void WriteStringValueSegment(ReadOnlySpan value, bool isFinalSegmen WriteStringSegmentEpilogue(); SetFlagToAddListSeparatorBeforeNextItem(); - PreviousSegmentEncoding = SegmentEncoding.None; + EnclosingContainerType container = _bitStack.Peek() ? EnclosingContainerType.Object : EnclosingContainerType.Array; + _enclosingContainer = _bitStack.CurrentDepth == 0 ? EnclosingContainerType.None : container; _tokenType = JsonTokenType.String; } } @@ -72,7 +64,7 @@ public void WriteStringValueSegment(ReadOnlySpan value, bool isFinalSegmen private void WriteStringSegmentWithLeftover(scoped ReadOnlySpan value, bool isFinalSegment) { Debug.Assert(HasPartialStringData); - Debug.Assert(PreviousSegmentEncoding == SegmentEncoding.Utf16); + Debug.Assert(_enclosingContainer == EnclosingContainerType.Utf16StringSequence); scoped ReadOnlySpan partialStringDataBuffer = PartialUtf16StringData; @@ -204,24 +196,15 @@ public void WriteStringValueSegment(ReadOnlySpan value, bool isFinalSegmen { JsonWriterHelper.ValidateValue(value); - if (_tokenType != Utf8JsonWriter.StringSegmentSentinel) + if (!_options.SkipValidation) { - Debug.Assert(PreviousSegmentEncoding == SegmentEncoding.None); - Debug.Assert(!HasPartialStringData); - - if (!_options.SkipValidation) - { - ValidateWritingValue(); - } - - WriteStringSegmentPrologue(); - - PreviousSegmentEncoding = SegmentEncoding.Utf8; - _tokenType = Utf8JsonWriter.StringSegmentSentinel; + ValidateWritingSegment(EnclosingContainerType.Utf8StringSequence); } - else + + if (_enclosingContainer != EnclosingContainerType.Utf8StringSequence) { - ValidateEncodingDidNotChange(SegmentEncoding.Utf8); + WriteStringSegmentPrologue(); + _enclosingContainer = EnclosingContainerType.Utf8StringSequence; } // The steps to write a string segment are to complete the previous partial code point @@ -240,7 +223,8 @@ public void WriteStringValueSegment(ReadOnlySpan value, bool isFinalSegmen WriteStringSegmentEpilogue(); SetFlagToAddListSeparatorBeforeNextItem(); - PreviousSegmentEncoding = SegmentEncoding.None; + EnclosingContainerType container = _bitStack.Peek() ? EnclosingContainerType.Object : EnclosingContainerType.Array; + _enclosingContainer = _bitStack.CurrentDepth == 0 ? EnclosingContainerType.None : container; _tokenType = JsonTokenType.String; } } @@ -248,7 +232,7 @@ public void WriteStringValueSegment(ReadOnlySpan value, bool isFinalSegmen private void WriteStringSegmentWithLeftover(scoped ReadOnlySpan utf8Value, bool isFinalSegment) { Debug.Assert(HasPartialStringData); - Debug.Assert(PreviousSegmentEncoding == SegmentEncoding.Utf8); + Debug.Assert(_enclosingContainer == EnclosingContainerType.Utf8StringSequence); scoped ReadOnlySpan partialStringDataBuffer = PartialUtf8StringData; @@ -379,24 +363,15 @@ public void WriteBase64StringSegment(ReadOnlySpan value, bool isFinalSegme ThrowHelper.ThrowArgumentException_ValueTooLarge(value.Length); } - if (_tokenType != Utf8JsonWriter.StringSegmentSentinel) + if (!_options.SkipValidation) { - Debug.Assert(PreviousSegmentEncoding == SegmentEncoding.None); - Debug.Assert(!HasPartialStringData); - - if (!_options.SkipValidation) - { - ValidateWritingValue(); - } - - WriteStringSegmentPrologue(); - - PreviousSegmentEncoding = SegmentEncoding.Base64; - _tokenType = Utf8JsonWriter.StringSegmentSentinel; + ValidateWritingSegment(EnclosingContainerType.Base64StringSequence); } - else + + if (_enclosingContainer != EnclosingContainerType.Base64StringSequence) { - ValidateEncodingDidNotChange(SegmentEncoding.Base64); + WriteStringSegmentPrologue(); + _enclosingContainer = EnclosingContainerType.Base64StringSequence; } // The steps to write a string segment are to complete the previous partial string data @@ -415,7 +390,8 @@ public void WriteBase64StringSegment(ReadOnlySpan value, bool isFinalSegme WriteStringSegmentEpilogue(); SetFlagToAddListSeparatorBeforeNextItem(); - PreviousSegmentEncoding = SegmentEncoding.None; + EnclosingContainerType container = _bitStack.Peek() ? EnclosingContainerType.Object : EnclosingContainerType.Array; + _enclosingContainer = _bitStack.CurrentDepth == 0 ? EnclosingContainerType.None : container; _tokenType = JsonTokenType.String; } } @@ -423,7 +399,7 @@ public void WriteBase64StringSegment(ReadOnlySpan value, bool isFinalSegme private void WriteBase64StringSegmentWithLeftover(scoped ReadOnlySpan bytes, bool isFinalSegment) { Debug.Assert(HasPartialStringData); - Debug.Assert(PreviousSegmentEncoding == SegmentEncoding.Base64); + Debug.Assert(_enclosingContainer == EnclosingContainerType.Base64StringSequence); scoped ReadOnlySpan partialStringDataBuffer = PartialBase64StringData; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.cs index 50c102a0b14eac..1fa6f661732d7e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.cs @@ -8,6 +8,7 @@ using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; namespace System.Text.Json { @@ -34,24 +35,13 @@ public sealed partial class Utf8JsonWriter : IDisposable, IAsyncDisposable private const int DefaultGrowthSize = 4096; private const int InitialGrowthSize = 256; - // A special value for JsonTokenType that lets the writer keep track of string segments. - private const JsonTokenType StringSegmentSentinel = (JsonTokenType)255; - - // Masks and flags for the length and encoding of the partial string data. - private const byte PartialStringDataLengthMask = 0b000_000_11; - private const byte PartialStringDataEncodingMask = 0b000_111_00; - - private const byte PartialStringDataUtf8EncodingFlag = 0b000_001_00; - private const byte PartialStringDataUtf16EncodingFlag = 0b000_010_00; - private const byte PartialStringDataBase64EncodingFlag = 0b000_100_00; - private IBufferWriter? _output; private Stream? _stream; private ArrayBufferWriter? _arrayBufferWriter; private Memory _memory; - private bool _inObject; + private EnclosingContainerType _enclosingContainer; private bool _commentAfterNoneOrPropertyName; private JsonTokenType _tokenType; private BitStack _bitStack; @@ -75,11 +65,9 @@ private struct Inline3ByteArray #endif /// - /// Stores the length and encoding of the partial string data. Outside of segment writes, this value is 0. - /// Across segment writes, this value is always non-zero even if the length is 0, to indicate the encoding of the segment. - /// This allows detection of encoding changes across segment writes. + /// Length of the partial string data. /// - private byte _partialStringDataFlags; + private byte _partialStringDataLength; // The highest order bit of _currentDepth is used to discern whether we are writing the first item in a list or not. // if (_currentDepth >> 31) == 1, add a list separator before writing the item @@ -127,15 +115,6 @@ private struct Inline3ByteArray /// public int CurrentDepth => _currentDepth & JsonConstants.RemoveFlagsBitMask; - /// - /// Length of the partial string data. - /// - private byte PartialStringDataLength - { - get => (byte)(_partialStringDataFlags & PartialStringDataLengthMask); - set => _partialStringDataFlags = (byte)((_partialStringDataFlags & ~PartialStringDataLengthMask) | value); - } - /// /// The partial UTF-8 code point. /// @@ -143,12 +122,12 @@ private ReadOnlySpan PartialUtf8StringData { get { - Debug.Assert(PreviousSegmentEncoding == SegmentEncoding.Utf8); + Debug.Assert(_enclosingContainer == EnclosingContainerType.Utf8StringSequence); ReadOnlySpan partialStringDataBytes = PartialStringDataRaw; Debug.Assert(partialStringDataBytes.Length == 3); - byte length = PartialStringDataLength; + byte length = _partialStringDataLength; Debug.Assert(length < 4); return partialStringDataBytes.Slice(0, length); @@ -161,7 +140,7 @@ private ReadOnlySpan PartialUtf8StringData Span partialStringDataBytes = PartialStringDataRaw; value.CopyTo(partialStringDataBytes); - PartialStringDataLength = (byte)value.Length; + _partialStringDataLength = (byte)value.Length; } } @@ -172,12 +151,12 @@ private ReadOnlySpan PartialUtf16StringData { get { - Debug.Assert(PreviousSegmentEncoding == SegmentEncoding.Utf16); + Debug.Assert(_enclosingContainer == EnclosingContainerType.Utf16StringSequence); ReadOnlySpan partialStringDataBytes = PartialStringDataRaw; Debug.Assert(partialStringDataBytes.Length == 3); - byte length = PartialStringDataLength; + byte length = _partialStringDataLength; Debug.Assert(length is 2 or 0); return MemoryMarshal.Cast(partialStringDataBytes.Slice(0, length)); @@ -189,7 +168,7 @@ private ReadOnlySpan PartialUtf16StringData Span partialStringDataBytes = PartialStringDataRaw; value.CopyTo(MemoryMarshal.Cast(partialStringDataBytes)); - PartialStringDataLength = (byte)(2 * value.Length); + _partialStringDataLength = (byte)(2 * value.Length); } } @@ -200,12 +179,12 @@ private ReadOnlySpan PartialBase64StringData { get { - Debug.Assert(PreviousSegmentEncoding == SegmentEncoding.Base64); + Debug.Assert(_enclosingContainer == EnclosingContainerType.Base64StringSequence); ReadOnlySpan partialStringDataBytes = PartialStringDataRaw; Debug.Assert(partialStringDataBytes.Length == 3); - byte length = PartialStringDataLength; + byte length = _partialStringDataLength; Debug.Assert(length < 3); return partialStringDataBytes.Slice(0, length); @@ -217,30 +196,10 @@ private ReadOnlySpan PartialBase64StringData Span partialStringDataBytes = PartialStringDataRaw; value.CopyTo(partialStringDataBytes); - PartialStringDataLength = (byte)value.Length; + _partialStringDataLength = (byte)value.Length; } } - /// - /// Encoding used for the previous string segment write. - /// - private SegmentEncoding PreviousSegmentEncoding - { - get => (SegmentEncoding)(_partialStringDataFlags & PartialStringDataEncodingMask); - set => _partialStringDataFlags = (byte)((_partialStringDataFlags & ~PartialStringDataEncodingMask) | (byte)value); - } - - /// - /// Convenience enumeration to track the encoding of the partial string data. This must be kept in sync with the PartialStringData*Encoding flags. - /// - internal enum SegmentEncoding : byte - { - None = 0, - Utf8 = PartialStringDataUtf8EncodingFlag, - Utf16 = PartialStringDataUtf16EncodingFlag, - Base64 = PartialStringDataBase64EncodingFlag, - } - private Utf8JsonWriter() { } @@ -412,7 +371,7 @@ private void ResetHelper() BytesCommitted = default; _memory = default; - _inObject = default; + _enclosingContainer = default; _tokenType = default; _commentAfterNoneOrPropertyName = default; _currentDepth = default; @@ -420,7 +379,7 @@ private void ResetHelper() _bitStack = default; _partialStringData = default; - _partialStringDataFlags = default; + _partialStringDataLength = default; } private void CheckNotDisposed() @@ -630,7 +589,9 @@ public void WriteStartObject() private void WriteStart(byte token) { if (CurrentDepth >= _options.MaxDepth) - ThrowHelper.ThrowInvalidOperationException(ExceptionResource.DepthTooLarge, _currentDepth, _options.MaxDepth, token: default, tokenType: default); + { + ThrowInvalidOperationException_DepthTooLarge(); + } if (_options.IndentedOrNotSkipValidation) { @@ -683,28 +644,38 @@ private void WriteStartSlow(byte token) } private void ValidateStart() + { + // Note that Start[Array|Object] indicates the start of a value, so the same check can be used. + if (!CanWriteValue) + { + OnValidateStartFailed(); + } + } + + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + private void OnValidateStartFailed() { // Make sure a new object or array is not attempted within an unfinalized string. - ValidateNotWithinUnfinalizedString(); + if (IsWritingPartialString) + { + ThrowInvalidOperationException(ExceptionResource.CannotWriteWithinString); + } + + Debug.Assert(!HasPartialStringData); - if (_inObject) + if (_enclosingContainer == EnclosingContainerType.Object) { - if (_tokenType != JsonTokenType.PropertyName) - { - Debug.Assert(_tokenType != JsonTokenType.None && _tokenType != JsonTokenType.StartArray); - ThrowHelper.ThrowInvalidOperationException(ExceptionResource.CannotStartObjectArrayWithoutProperty, currentDepth: default, maxDepth: _options.MaxDepth, token: default, _tokenType); - } + Debug.Assert(_tokenType != JsonTokenType.PropertyName); + Debug.Assert(_tokenType != JsonTokenType.None && _tokenType != JsonTokenType.StartArray); + ThrowInvalidOperationException(ExceptionResource.CannotStartObjectArrayWithoutProperty); } else { Debug.Assert(_tokenType != JsonTokenType.PropertyName); Debug.Assert(_tokenType != JsonTokenType.StartObject); - - // It is more likely for CurrentDepth to not equal 0 when writing valid JSON, so check that first to rely on short-circuiting and return quickly. - if (CurrentDepth == 0 && _tokenType != JsonTokenType.None) - { - ThrowHelper.ThrowInvalidOperationException(ExceptionResource.CannotStartObjectArrayAfterPrimitiveOrClose, currentDepth: default, maxDepth: _options.MaxDepth, token: default, _tokenType); - } + Debug.Assert(CurrentDepth == 0 && _tokenType != JsonTokenType.None); + ThrowInvalidOperationException(ExceptionResource.CannotStartObjectArrayAfterPrimitiveOrClose); } } @@ -1110,33 +1081,32 @@ private void WriteEndSlow(byte token) } } + // Performance degrades significantly in some scenarios when inlining is allowed. + [MethodImpl(MethodImplOptions.NoInlining)] private void ValidateEnd(byte token) { - // Make sure an object is not ended within an unfinalized string. - ValidateNotWithinUnfinalizedString(); - - if (_bitStack.CurrentDepth <= 0 || _tokenType == JsonTokenType.PropertyName) - ThrowHelper.ThrowInvalidOperationException(ExceptionResource.MismatchedObjectArray, currentDepth: default, maxDepth: _options.MaxDepth, token, _tokenType); + if (_tokenType == JsonTokenType.PropertyName) + ThrowInvalidOperationException_MismatchedObjectArray(token); if (token == JsonConstants.CloseBracket) { - if (_inObject) + if (_enclosingContainer != EnclosingContainerType.Array) { - Debug.Assert(_tokenType != JsonTokenType.None); - ThrowHelper.ThrowInvalidOperationException(ExceptionResource.MismatchedObjectArray, currentDepth: default, maxDepth: _options.MaxDepth, token, _tokenType); + ThrowInvalidOperationException_MismatchedObjectArray(token); } } else { Debug.Assert(token == JsonConstants.CloseBrace); - if (!_inObject) + if (_enclosingContainer != EnclosingContainerType.Object) { - ThrowHelper.ThrowInvalidOperationException(ExceptionResource.MismatchedObjectArray, currentDepth: default, maxDepth: _options.MaxDepth, token, _tokenType); + ThrowInvalidOperationException_MismatchedObjectArray(token); } } - _inObject = _bitStack.Pop(); + EnclosingContainerType container = _bitStack.Pop() ? EnclosingContainerType.Object : EnclosingContainerType.Array; + _enclosingContainer = _bitStack.CurrentDepth == 0 ? EnclosingContainerType.None : container; } private void WriteEndIndented(byte token) @@ -1202,13 +1172,13 @@ private void UpdateBitStackOnStart(byte token) if (token == JsonConstants.OpenBracket) { _bitStack.PushFalse(); - _inObject = false; + _enclosingContainer = EnclosingContainerType.Array; } else { Debug.Assert(token == JsonConstants.OpenBrace); _bitStack.PushTrue(); - _inObject = true; + _enclosingContainer = EnclosingContainerType.Object; } } @@ -1284,7 +1254,72 @@ private void SetFlagToAddListSeparatorBeforeNextItem() _currentDepth |= 1 << 31; } + [MethodImpl(MethodImplOptions.NoInlining)] + [DoesNotReturn] + private void ThrowInvalidOperationException(ExceptionResource resource) + => ThrowHelper.ThrowInvalidOperationException(resource, currentDepth: default, maxDepth: _options.MaxDepth, token: default, _tokenType); + + [MethodImpl(MethodImplOptions.NoInlining)] + [DoesNotReturn] + private void ThrowInvalidOperationException_MismatchedObjectArray(byte token) + => ThrowHelper.ThrowInvalidOperationException(ExceptionResource.MismatchedObjectArray, currentDepth: default, maxDepth: _options.MaxDepth, token, _tokenType); + + [MethodImpl(MethodImplOptions.NoInlining)] + [DoesNotReturn] + private void ThrowInvalidOperationException_DepthTooLarge() + => ThrowHelper.ThrowInvalidOperationException(ExceptionResource.DepthTooLarge, _currentDepth, _options.MaxDepth, token: default, tokenType: default); + [DebuggerBrowsable(DebuggerBrowsableState.Never)] private string DebuggerDisplay => $"BytesCommitted = {BytesCommitted} BytesPending = {BytesPending} CurrentDepth = {CurrentDepth}"; + + /// + /// Indicates whether the writer is currently writing a partial string value. + /// + private bool IsWritingPartialString => _enclosingContainer >= EnclosingContainerType.Utf8StringSequence; + + /// + /// The type of container that is enclosing the current position. The underlying values have been chosen + /// to allow to be done using bitwise operations and must be kept in sync with . + /// + internal enum EnclosingContainerType : byte + { + /// + /// Root level. The choice of allows fast validation by equality comparison when writing values + /// since a value can be written at the root level only if there was no previous token. + /// + None = JsonTokenType.None, + + /// + /// JSON object. The choice of allows fast validation by equality comparison when writing values + /// since a value can be written inside a JSON object only if the previous token is a property name. + /// + Object = JsonTokenType.PropertyName, + + /// + /// JSON array. Chosen so that its lower nibble is 0 to ensure it does not conflict with numeric values that currently are less than 16. + /// + Array = 0x10, + + /// + /// Partial UTF-8 string. This is a container if viewed as an array of "utf-8 string segment"-typed values. This array can only be one level deep + /// so does not need to store its state. + /// relies on the value of the partial string members being the largest values of this enum. + /// + Utf8StringSequence = 0x20, + + /// + /// Partial UTF-16 string. This is a container if viewed as an array of "utf-16 string segment"-typed values. This array can only be one level deep + /// so does not need to store its state. + /// relies on the value of the partial string members being the largest values of this enum. + /// + Utf16StringSequence = 0x30, + + /// + /// Partial Base64 string. This is a container if viewed as an array of "base64 string segment"-typed values. This array can only be one level deep + /// so does not need to store its state. + /// relies on the value of the partial string members being the largest values of this enum. + /// + Base64StringSequence = 0x40, + } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonWriterTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonWriterTests.cs index b9564958092f9a..5abafb3914fbf1 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonWriterTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonWriterTests.cs @@ -2935,6 +2935,64 @@ public void InvalidJsonStringValueSegment(string _, Action write Assert.Throws(() => write(jsonUtf8)); } } + + using (var jsonUtf8 = new Utf8JsonWriter(output, options)) + { + jsonUtf8.WriteStartArray(); + jsonUtf8.WriteStringValueSegment("foo"u8, isFinalSegment: false); + if (options.SkipValidation) + { + write(jsonUtf8); + } + else + { + Assert.Throws(() => write(jsonUtf8)); + } + } + + using (var jsonUtf8 = new Utf8JsonWriter(output, options)) + { + jsonUtf8.WriteStartArray(); + jsonUtf8.WriteStringValueSegment("foo".ToCharArray(), isFinalSegment: false); + if (options.SkipValidation) + { + write(jsonUtf8); + } + else + { + Assert.Throws(() => write(jsonUtf8)); + } + } + + using (var jsonUtf8 = new Utf8JsonWriter(output, options)) + { + jsonUtf8.WriteStartObject(); + jsonUtf8.WritePropertyName("prop"); + jsonUtf8.WriteStringValueSegment("foo"u8, isFinalSegment: false); + if (options.SkipValidation) + { + write(jsonUtf8); + } + else + { + Assert.Throws(() => write(jsonUtf8)); + } + } + + using (var jsonUtf8 = new Utf8JsonWriter(output, options)) + { + jsonUtf8.WriteStartObject(); + jsonUtf8.WritePropertyName("prop"); + jsonUtf8.WriteStringValueSegment("foo".ToCharArray(), isFinalSegment: false); + if (options.SkipValidation) + { + write(jsonUtf8); + } + else + { + Assert.Throws(() => write(jsonUtf8)); + } + } } [Theory]