diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/VersionConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/VersionConverter.cs index 5f3757bb8ae926..7e135cf6cb9118 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/VersionConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/VersionConverter.cs @@ -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; @@ -10,11 +11,7 @@ namespace System.Text.Json.Serialization.Converters internal sealed class VersionConverter : JsonPrimitiveConverter { #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) @@ -37,31 +34,43 @@ 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)) + int bufferLength = reader.ValueLength; + char[]? rentedBuffer = null; + try { - ThrowHelper.ThrowFormatException(DataType.Version); + Span charBuffer = bufferLength <= JsonConstants.StackallocCharThreshold + ? stackalloc char[JsonConstants.StackallocCharThreshold] + : (rentedBuffer = ArrayPool.Shared.Rent(bufferLength)); + + int bytesWritten = reader.CopyString(charBuffer); + ReadOnlySpan 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!; + } } - - Span charBuffer = stackalloc char[MaximumEscapedVersionLength]; - int bytesWritten = reader.CopyString(charBuffer); - ReadOnlySpan source = charBuffer.Slice(0, bytesWritten); - - if (!char.IsDigit(source[0]) || !char.IsDigit(source[^1])) + finally { - // 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(source, out Version? result)) - { - return result; + if (rentedBuffer is not null) + { + ArrayPool.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, @@ -69,6 +78,7 @@ private static Version ReadCore(ref Utf8JsonReader reader) // since Version.TryParse allows them and silently parses input to Version ThrowHelper.ThrowFormatException(DataType.Version); } + if (Version.TryParse(versionString, out Version? result)) { return result; @@ -88,12 +98,11 @@ public override void Write(Utf8JsonWriter writer, Version? value, JsonSerializer #if NET #if NET8_0_OR_GREATER - Span span = stackalloc byte[MaximumVersionLength]; + Span span = stackalloc byte[MaximumFormattedVersionLength]; #else - Span span = stackalloc char[MaximumVersionLength]; + Span 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()); @@ -111,12 +120,11 @@ internal override void WriteAsPropertyNameCore(Utf8JsonWriter writer, Version va #if NET #if NET8_0_OR_GREATER - Span span = stackalloc byte[MaximumVersionLength]; + Span span = stackalloc byte[MaximumFormattedVersionLength]; #else - Span span = stackalloc char[MaximumVersionLength]; + Span 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()); diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Value.ReadTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Value.ReadTests.cs index 1e1b585ea33c47..d235b69f9bfff6 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Value.ReadTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Value.ReadTests.cs @@ -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 public static void Version_Read_Success(string json, string? actual = null) { actual ??= json; @@ -348,6 +353,9 @@ 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")] @@ -355,9 +363,6 @@ public static void Version_Read_Success(string json, string? actual = null) [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)]