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]