Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers;
using System.Diagnostics;
using System.Text.Json.Nodes;
using System.Text.Json.Schema;
Expand All @@ -10,11 +11,7 @@ namespace System.Text.Json.Serialization.Converters
internal sealed class VersionConverter : JsonPrimitiveConverter<Version?>
{
#if NET
private const int MinimumVersionLength = 3; // 0.0

private const int MaximumVersionLength = 43; // 2147483647.2147483647.2147483647.2147483647

private const int MaximumEscapedVersionLength = JsonConstants.MaxExpansionFactorWhileEscaping * MaximumVersionLength;
private const int MaximumFormattedVersionLength = 43; // 2147483647.2147483647.2147483647.2147483647
#endif

public override Version? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
Expand All @@ -37,38 +34,75 @@ private static Version ReadCore(ref Utf8JsonReader reader)
Debug.Assert(reader.TokenType is JsonTokenType.PropertyName or JsonTokenType.String);

#if NET
if (!JsonHelpers.IsInRangeInclusive(reader.ValueLength, MinimumVersionLength, MaximumEscapedVersionLength))
{
ThrowHelper.ThrowFormatException(DataType.Version);
}

Span<char> charBuffer = stackalloc char[MaximumEscapedVersionLength];
int bytesWritten = reader.CopyString(charBuffer);
ReadOnlySpan<char> source = charBuffer.Slice(0, bytesWritten);

if (!char.IsDigit(source[0]) || !char.IsDigit(source[^1]))
#if NET10_0_OR_GREATER
// .NET 10+ optimization: parse directly from UTF8 bytes when the value is in a single span
if (!reader.HasValueSequence && !reader.ValueIsEscaped)
{
// Since leading and trailing whitespaces are forbidden throughout System.Text.Json converters
// we need to make sure that our input doesn't have them,
// and if it has - we need to throw, to match behaviour of other converters
// since Version.TryParse allows them and silently parses input to Version
ThrowHelper.ThrowFormatException(DataType.Version);
ReadOnlySpan<byte> utf8Source = reader.ValueSpan;

if (utf8Source.IsEmpty || utf8Source[0] == (byte)' ' || utf8Source[^1] == (byte)' ')
{
// Since leading and trailing whitespaces are forbidden throughout System.Text.Json converters
// we need to make sure that our input doesn't have them,
// and if it has - we need to throw, to match behaviour of other converters
// since Version.TryParse allows them and silently parses input to Version
ThrowHelper.ThrowFormatException(DataType.Version);
}

if (Version.TryParse(utf8Source, out Version? result))
{
return result!;
}
}

if (Version.TryParse(source, out Version? result))
else
#endif
{
return result;
int bufferLength = reader.ValueLength;
char[]? rentedBuffer = null;
try
{
Span<char> charBuffer = bufferLength <= JsonConstants.StackallocCharThreshold
? stackalloc char[JsonConstants.StackallocCharThreshold]
: (rentedBuffer = ArrayPool<char>.Shared.Rent(bufferLength));

int bytesWritten = reader.CopyString(charBuffer);
ReadOnlySpan<char> source = charBuffer.Slice(0, bytesWritten);

if (source.IsEmpty || char.IsWhiteSpace(source[0]) || char.IsWhiteSpace(source[^1]))
{
// Since leading and trailing whitespaces are forbidden throughout System.Text.Json converters
// we need to make sure that our input doesn't have them,
// and if it has - we need to throw, to match behaviour of other converters
// since Version.TryParse allows them and silently parses input to Version
ThrowHelper.ThrowFormatException(DataType.Version);
}

bool success = Version.TryParse(source, out Version? result);

if (success)
{
return result!;
}
}
finally
{
if (rentedBuffer is not null)
{
ArrayPool<char>.Shared.Return(rentedBuffer);
}
}
}
#else
string? versionString = reader.GetString();
if (!string.IsNullOrEmpty(versionString) && (!char.IsDigit(versionString[0]) || !char.IsDigit(versionString[versionString.Length - 1])))
if (string.IsNullOrEmpty(versionString) || char.IsWhiteSpace(versionString[0]) || char.IsWhiteSpace(versionString[versionString.Length - 1]))
{
// Since leading and trailing whitespaces are forbidden throughout System.Text.Json converters
// we need to make sure that our input doesn't have them,
// and if it has - we need to throw, to match behaviour of other converters
// since Version.TryParse allows them and silently parses input to Version
ThrowHelper.ThrowFormatException(DataType.Version);
}

if (Version.TryParse(versionString, out Version? result))
{
return result;
Expand All @@ -88,12 +122,11 @@ public override void Write(Utf8JsonWriter writer, Version? value, JsonSerializer

#if NET
#if NET8_0_OR_GREATER
Span<byte> span = stackalloc byte[MaximumVersionLength];
Span<byte> span = stackalloc byte[MaximumFormattedVersionLength];
#else
Span<char> span = stackalloc char[MaximumVersionLength];
Span<char> span = stackalloc char[MaximumFormattedVersionLength];
#endif
bool formattedSuccessfully = value.TryFormat(span, out int charsWritten);
Debug.Assert(formattedSuccessfully && charsWritten >= MinimumVersionLength);
writer.WriteStringValue(span.Slice(0, charsWritten));
#else
writer.WriteStringValue(value.ToString());
Expand All @@ -111,12 +144,11 @@ internal override void WriteAsPropertyNameCore(Utf8JsonWriter writer, Version va

#if NET
#if NET8_0_OR_GREATER
Span<byte> span = stackalloc byte[MaximumVersionLength];
Span<byte> span = stackalloc byte[MaximumFormattedVersionLength];
#else
Span<char> span = stackalloc char[MaximumVersionLength];
Span<char> span = stackalloc char[MaximumFormattedVersionLength];
#endif
bool formattedSuccessfully = value.TryFormat(span, out int charsWritten);
Debug.Assert(formattedSuccessfully && charsWritten >= MinimumVersionLength);
writer.WritePropertyName(span.Slice(0, charsWritten));
#else
writer.WritePropertyName(value.ToString());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,11 @@ public static void ValueFail()
[InlineData("2147483647.2147483647.2147483647.2147483647")]
[InlineData("\\u0032\\u0031\\u0034\\u0037\\u0034\\u0038\\u0033\\u0036\\u0034\\u0037\\u002e\\u0032\\u0031\\u0034\\u0037\\u0034\\u0038\\u0033\\u0036\\u0034\\u0037\\u002e\\u0032\\u0031\\u0034\\u0037\\u0034\\u0038\\u0033\\u0036\\u0034\\u0037\\u002e\\u0032\\u0031\\u0034\\u0037\\u0034\\u0038\\u0033\\u0036\\u0034\\u0037",
"2147483647.2147483647.2147483647.2147483647")]
[InlineData("1.+1", "1.1")] // Plus in components should work as before
[InlineData("1 .1", "1.1")] // Whitespace before dot should work as before
[InlineData("1. 1", "1.1")] // Whitespace after dot should work as before
[InlineData("1 . +1", "1.1")] // Combined whitespace and plus should work as before
[InlineData("+1.1", "1.1")] // Leading plus should work
Copy link
Contributor

Choose a reason for hiding this comment

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

Also test against +2147483647 . +2147483647 . +2147483647 . +2147483647?

Copy link
Member

Choose a reason for hiding this comment

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

@copilot ☝️

public static void Version_Read_Success(string json, string? actual = null)
{
actual ??= json;
Expand All @@ -348,16 +353,16 @@ public static void Version_Read_Success(string json, string? actual = null)
[InlineData("")]
[InlineData(" ")]
[InlineData(" ")]
[InlineData(" 1.2.3.4")] // Leading whitespace should be rejected
[InlineData("1.2.3.4 ")] // Trailing whitespace should be rejected
[InlineData(" 1.2.3.4 ")] // Leading and trailing whitespace should be rejected
[InlineData("2147483648.2147483648.2147483648.2147483648")] //int.MaxValue + 1
[InlineData("2147483647.2147483647.2147483647.21474836477")] // Slightly bigger in size than max length of Version
[InlineData("-2147483648.-2147483648")]
[InlineData("-2147483648.-2147483648.-2147483648")]
[InlineData("-2147483648.-2147483648.-2147483648.-2147483648")]
[InlineData("1.-1")]
[InlineData("1")]
[InlineData(" 1.2.3.4")] //Valid but has leading whitespace
[InlineData("1.2.3.4 ")] //Valid but has trailing whitespace
[InlineData(" 1.2.3.4 ")] //Valid but has trailing and leading whitespaces
[InlineData("{}", false)]
[InlineData("[]", false)]
[InlineData("true", false)]
Expand Down
Loading