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 Expand Up @@ -110,13 +110,13 @@ public bool Pop()
}
else
{
inObject = PopFromArray();
inObject = PeekInArray();
}
return inObject;
}

[MethodImpl(MethodImplOptions.NoInlining)]
private bool PopFromArray()
private readonly bool PeekInArray()
Copy link
Member

@eiriktsarpalis eiriktsarpalis Jan 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are we peeking at in this case? It seems we're just returning a boolean so perhaps this could be expressed as a property, i.e. IsInArray or something?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks at the top of the stack assuming the stack is backed by an array. IsInArray sounds like it's checking containment. I also like it being a method since it's symmetric with Push/PushToArray and Peek/PeekInArray. I'm open to better naming but I left it as is with more comments.

{
int index = _currentDepth - AllocationFreeMaxDepth - 1;
Debug.Assert(_array != null);
Expand All @@ -129,6 +129,8 @@ private bool PopFromArray()
return (_array[elementIndex] & (1 << extraBits)) != 0;
}

public readonly bool Peek() => _currentDepth <= AllocationFreeMaxDepth ? (_allocationFreeContainer & 1) != 0 : PeekInArray();

private void DoubleArray(int minSize)
{
Debug.Assert(_array != null);
Expand Down
19 changes: 17 additions & 2 deletions src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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.PartialUtf8String: return "UTF-8";
case EnclosingContainerType.PartialUtf16String: return "UTF-16";
case EnclosingContainerType.PartialBase64String: return "Base64";
default:
Debug.Fail("Unknown encoding.");
return "Unknown";
};
}
}

private static InvalidOperationException GetInvalidOperationException(string message, JsonTokenType tokenType)
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)
{
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)
{
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<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,54 +12,113 @@ namespace System.Text.Json
{
public sealed partial class Utf8JsonWriter
{
private bool HasPartialStringData => PartialStringDataLength != 0;
/// <summary>
/// Assuming that the writer is currently in a valid state, this returns true if a JSON value is allowed at the current position.
/// <list type="bullet">
/// <item>
/// If <see cref="_enclosingContainer"/> is an array then writing a value is always allowed.
/// </item>
/// <item>
/// If <see cref="_enclosingContainer"/> is an object then writing a value is allowed only if <see cref="_tokenType"/> is a property name.
/// Because we designed <see cref="EnclosingContainerType.Object"/> == <see cref="JsonTokenType.PropertyName"/>, we can just check for equality.
/// </item>
/// <item>
/// If <see cref="_enclosingContainer"/> is none (the root level) then writing a value is allowed only if <see cref="_tokenType"/> is None (only
/// one value may be written at the root). This case is identical to the previous case.
/// </item>
/// <item>
/// If <see cref="_enclosingContainer"/> is a partial value, then it will never be a valid <see cref="_tokenType"/> by construction.
/// </item>
/// </list>
/// This method performs better without short circuiting (this often gets inlined so using simple branch free code seems to have some benefits).
/// </summary>
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)]
Copy link
Contributor

@pentp pentp Jan 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NoInlining should not be applied to throw helpers, otherwise RyuJIt will not detect the callsite as cold code.

These helpers should probably follow the pattern like throw GetInvalidOperationException(); to make them small, but with no return (thus not inlined), so the callsites can be optimized properly.

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);
// 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)]
Expand Down
Loading
Loading