Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ internal struct BitStack

private int _currentDepth;

public int CurrentDepth => _currentDepth;
public readonly int CurrentDepth => _currentDepth;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void PushTrue()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Buffers;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

Expand Down Expand Up @@ -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 || _tokenType == StringSegmentSentinel)
{
Debug.Assert(_tokenType != JsonTokenType.StartObject);
ThrowHelper.ThrowInvalidOperationException(ExceptionResource.CannotWritePropertyWithinArray, currentDepth: default, maxDepth: _options.MaxDepth, token: default, _tokenType);
OnValidateWritingPropertyFailed();
}
}
}
Expand All @@ -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 || _tokenType == StringSegmentSentinel)
{
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 (_enclosingContainer != EnclosingContainerType.Object || _tokenType == JsonTokenType.PropertyName)
{
ThrowInvalidOperationException(ExceptionResource.CannotWritePropertyWithinArray);
}

Debug.Assert(_tokenType == StringSegmentSentinel);
ThrowInvalidOperationException(ExceptionResource.CannotWriteWithinString);
}

private void WritePropertyNameMinimized(ReadOnlySpan<byte> escapedPropertyName, byte token)
{
Debug.Assert(escapedPropertyName.Length < int.MaxValue - 5);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,29 @@ namespace System.Text.Json
{
public sealed partial class Utf8JsonWriter
{
/// <summary>
/// Assuming that the writer is currently in a valid state, this returns true if a JSON value is not allowed at the current position.
/// Note that every JsonTokenType is less than 16 (0b0001_0000) except string segment (which is 0b0010_0000), so for these tokens only the
/// low nibble needs to be checked. There are 3 cases to consider:
/// <list type="bullet">
/// <item>
/// The writer is in an array (<see cref="_enclosingContainer"/> = 0b0001_0000): The only invalid previous token is a string segment.
/// <see cref="_enclosingContainer"/> ^ 0b0001_0000 is 0, so the entire expression is <see cref="_tokenType"/> > 0b0001_0000, which is true iff the previous token is a string segment.
/// </item>
/// <item>
/// The writer is at the root level (<see cref="_enclosingContainer"/> = 0). The only valid previous token is none. <see cref="_enclosingContainer"/> ^ 0b0001_0000 = 0b0001_0000,
/// so the entire expression is 0b0001_0000 ^ <see cref="_tokenType"/> > 0b0001_0000. For string segment this is true, and for all other tokens we just need to check the low
/// nibble. 0000 ^ wxyz = 0 iff wxyz = 0000, which is JsonTokenType.None. For every other token, the inequality is true.
/// </item>
/// <item>
/// The writer is in an object (<see cref="_enclosingContainer"/> = 0b0000_0101). The only valid previous token is a property. <see cref="_enclosingContainer"/> ^ 0b0001_0000 = 0b0001_0101,
/// so the entire expression is 0b0001_0101 ^ <see cref="_tokenType"/> > 0b0001_0000. For string segment this inequality is true. For every other token, we just need
/// to check the low nibble. 0101 ^ wxyz = 0 iff wxyz = 0101, which is JsonTokenType.PropertyName. For every other token, the inequality is true.
/// </item>
/// </list>
/// </summary>
private bool CannotWriteValue => (0b0001_0000 ^ (byte)_enclosingContainer ^ (byte)_tokenType) > 0b0001_0000;

private bool HasPartialCodePoint => PartialCodePointLength != 0;

private void ClearPartialCodePoint() => PartialCodePointLength = 0;
Expand All @@ -28,37 +51,46 @@ private void ValidateNotWithinUnfinalizedString()
{
if (_tokenType == StringSegmentSentinel)
{
ThrowHelper.ThrowInvalidOperationException(ExceptionResource.CannotWriteWithinString, currentDepth: default, maxDepth: _options.MaxDepth, token: default, _tokenType);
ThrowInvalidOperationException(ExceptionResource.CannotWriteWithinString);
}

Debug.Assert(PreviousSegmentEncoding == SegmentEncoding.None);
Debug.Assert(!HasPartialCodePoint);
}

private void ValidateWritingValue()
{
if (CannotWriteValue)
{
OnValidateWritingValueFailed();
}
}

[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
private void OnValidateWritingValueFailed()
{
Debug.Assert(!_options.SkipValidation);

// Make sure a new value is not attempted within an unfinalized string.
ValidateNotWithinUnfinalizedString();
if (_tokenType == StringSegmentSentinel)
{
ThrowInvalidOperationException(ExceptionResource.CannotWriteWithinString);
}

Debug.Assert(PreviousSegmentEncoding == SegmentEncoding.None);
Debug.Assert(!HasPartialCodePoint);

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);

// 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);
}
Debug.Assert(CurrentDepth == 0 && _tokenType != JsonTokenType.None);
ThrowInvalidOperationException(ExceptionResource.CannotWriteValueAfterPrimitiveOrClose);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Threading;
using System.Threading.Tasks;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;

namespace System.Text.Json
{
Expand Down Expand Up @@ -36,7 +37,7 @@ public sealed partial class Utf8JsonWriter : IDisposable, IAsyncDisposable
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;
private const JsonTokenType StringSegmentSentinel = (JsonTokenType)0b0010_0000;

// Masks and flags for the length and encoding of the partial code point
private const byte PartialCodePointLengthMask = 0b000_000_11;
Expand All @@ -51,7 +52,7 @@ public sealed partial class Utf8JsonWriter : IDisposable, IAsyncDisposable

private Memory<byte> _memory;

private bool _inObject;
private EnclosingContainerType _enclosingContainer;
private bool _commentAfterNoneOrPropertyName;
private JsonTokenType _tokenType;
private BitStack _bitStack;
Expand Down Expand Up @@ -383,7 +384,7 @@ private void ResetHelper()
BytesCommitted = default;
_memory = default;

_inObject = default;
_enclosingContainer = default;
_tokenType = default;
_commentAfterNoneOrPropertyName = default;
_currentDepth = default;
Expand Down Expand Up @@ -601,7 +602,7 @@ 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)
{
Expand Down Expand Up @@ -654,28 +655,39 @@ 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 (CannotWriteValue)
{
OnValidateStartFailed();
}
}

[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
private void OnValidateStartFailed()
{
// Make sure a new object or array is not attempted within an unfinalized string.
ValidateNotWithinUnfinalizedString();
if (_tokenType == StringSegmentSentinel)
{
ThrowInvalidOperationException(ExceptionResource.CannotWriteWithinString);
}

Debug.Assert(PreviousSegmentEncoding == SegmentEncoding.None);
Debug.Assert(!HasPartialCodePoint);

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);
}
}

Expand Down Expand Up @@ -1081,33 +1093,33 @@ 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 (_bitStack.CurrentDepth <= 0 || _tokenType == JsonTokenType.PropertyName || _tokenType == StringSegmentSentinel)
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)
Expand Down Expand Up @@ -1173,13 +1185,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;
}
}

Expand Down Expand Up @@ -1255,7 +1267,44 @@ 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}";

/// <summary>
/// The type of container that is enclosing the current position. The underlying values have been chosen
/// to allow validation to be done using bitwise operations and must be kept in sync with JsonTokenType.
/// </summary>
private enum EnclosingContainerType : byte
{
/// <summary>
/// Root
/// </summary>
None = 0b0000_0000,

/// <summary>
/// JSON object. Note that this is the same value as JsonTokenType.PropertyName. See <see cref="CannotWriteValue"/> for more details.
/// </summary>
Object = 0b0000_0101,

/// <summary>
/// JSON array
/// </summary>
Array = 0b0001_0000,
}
}
}
Loading
Loading