diff --git a/src/libraries/System.Private.CoreLib/src/System/Version.cs b/src/libraries/System.Private.CoreLib/src/System/Version.cs index 9f2ee94f70770c..502a51ff60d10e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Version.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Version.cs @@ -8,6 +8,7 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Text; namespace System { @@ -387,7 +388,7 @@ static bool IUtf8SpanParsable.TryParse(ReadOnlySpan utf8Text, IFo int minor, build, revision; // Parse the major version - if (!TryParseComponent(input.Slice(0, majorEnd), nameof(input), throwOnFailure, out int major)) + if (!TryParseComponent(input.Slice(0, majorEnd), nameof(input), throwOnFailure, input, out int major)) { return null; } @@ -395,7 +396,7 @@ static bool IUtf8SpanParsable.TryParse(ReadOnlySpan utf8Text, IFo if (minorEnd != -1) { // If there's more than a major and minor, parse the minor, too. - if (!TryParseComponent(input.Slice(majorEnd + 1, minorEnd - majorEnd - 1), nameof(input), throwOnFailure, out minor)) + if (!TryParseComponent(input.Slice(majorEnd + 1, minorEnd - majorEnd - 1), nameof(input), throwOnFailure, input, out minor)) { return null; } @@ -404,15 +405,15 @@ static bool IUtf8SpanParsable.TryParse(ReadOnlySpan utf8Text, IFo { // major.minor.build.revision return - TryParseComponent(input.Slice(minorEnd + 1, buildEnd - minorEnd - 1), nameof(build), throwOnFailure, out build) && - TryParseComponent(input.Slice(buildEnd + 1), nameof(revision), throwOnFailure, out revision) ? + TryParseComponent(input.Slice(minorEnd + 1, buildEnd - minorEnd - 1), nameof(build), throwOnFailure, input, out build) && + TryParseComponent(input.Slice(buildEnd + 1), nameof(revision), throwOnFailure, input, out revision) ? new Version(major, minor, build, revision) : null; } else { // major.minor.build - return TryParseComponent(input.Slice(minorEnd + 1), nameof(build), throwOnFailure, out build) ? + return TryParseComponent(input.Slice(minorEnd + 1), nameof(build), throwOnFailure, input, out build) ? new Version(major, minor, build) : null; } @@ -420,24 +421,45 @@ static bool IUtf8SpanParsable.TryParse(ReadOnlySpan utf8Text, IFo else { // major.minor - return TryParseComponent(input.Slice(majorEnd + 1), nameof(input), throwOnFailure, out minor) ? + return TryParseComponent(input.Slice(majorEnd + 1), nameof(input), throwOnFailure, input, out minor) ? new Version(major, minor) : null; } } - private static bool TryParseComponent(ReadOnlySpan component, string componentName, bool throwOnFailure, out int parsedComponent) + private static bool TryParseComponent(ReadOnlySpan component, string componentName, bool throwOnFailure, ReadOnlySpan originalInput, out int parsedComponent) where TChar : unmanaged, IUtfChar { - if (throwOnFailure) + Number.ParsingStatus parseStatus = Number.TryParseBinaryIntegerStyle(component, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out parsedComponent); + + if (parseStatus == Number.ParsingStatus.OK && parsedComponent >= 0) { - parsedComponent = Number.ParseBinaryInteger(component, NumberStyles.Integer, NumberFormatInfo.InvariantInfo); - ArgumentOutOfRangeException.ThrowIfNegative(parsedComponent, componentName); return true; } - Number.ParsingStatus parseStatus = Number.TryParseBinaryIntegerStyle(component, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out parsedComponent); - return parseStatus == Number.ParsingStatus.OK && parsedComponent >= 0; + if (throwOnFailure) + { + ThrowFailure(parseStatus, parsedComponent, componentName, originalInput); + } + + return false; + + [DoesNotReturn] + static void ThrowFailure(Number.ParsingStatus parseStatus, int parsedComponent, string componentName, ReadOnlySpan originalInput) + { + ArgumentOutOfRangeException.ThrowIfNegative(parsedComponent, componentName); + + if (parseStatus == Number.ParsingStatus.Overflow) + { + Number.ThrowOverflowException(); + } + + string inputString = typeof(TChar) == typeof(char) ? + Unsafe.BitCast, ReadOnlySpan>(originalInput).ToString() : + Encoding.UTF8.GetString(Unsafe.BitCast, ReadOnlySpan>(originalInput)); + + throw new FormatException(SR.Format(SR.Format_InvalidStringWithValue, inputString)); + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/VersionTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/VersionTests.cs index 4bcb5fc5c41c2b..6a9a088341c9fa 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/VersionTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/VersionTests.cs @@ -283,6 +283,63 @@ public static void Parse_InvalidInput_ThrowsException(string input, Type excepti Assert.Null(version); } + [Theory] + [InlineData(".")] + [InlineData("1.")] + [InlineData("1.0.")] + [InlineData("1.0.0.")] + public static void Parse_TrailingDot_ThrowsFormatExceptionWithOriginalInput(string input) + { + FormatException ex = Assert.Throws(() => Version.Parse(input)); + Assert.Contains(input, ex.Message); + + Assert.False(Version.TryParse(input, out Version version)); + Assert.Null(version); + } + + [Theory] + [InlineData(".")] + [InlineData("1.")] + [InlineData("1.0.")] + [InlineData("1.0.0.")] + public static void Parse_Span_TrailingDot_ThrowsFormatExceptionWithOriginalInput(string input) + { + FormatException ex = Assert.Throws(() => Version.Parse(input.AsSpan())); + Assert.Contains(input, ex.Message); + + Assert.False(Version.TryParse(input.AsSpan(), out Version version)); + Assert.Null(version); + } + + [Theory] + [InlineData(".")] + [InlineData("1.")] + [InlineData("1.0.")] + [InlineData("1.0.0.")] + public static void Parse_Utf8_TrailingDot_ThrowsFormatExceptionWithOriginalInput(string input) + { + byte[] utf8Bytes = Encoding.UTF8.GetBytes(input); + + FormatException ex = Assert.Throws(() => Version.Parse(utf8Bytes)); + Assert.Contains(input, ex.Message); + + Assert.False(Version.TryParse(utf8Bytes, out Version version)); + Assert.Null(version); + } + + [Theory] + [InlineData(new byte[] { 0xFF, 0x2E, 0x30 })] // Invalid UTF8 start byte followed by ".0" + [InlineData(new byte[] { 0x31, 0x2E, 0xFF })] // "1." followed by invalid UTF8 byte + [InlineData(new byte[] { 0xC0, 0x80, 0x2E, 0x30 })] // Overlong encoding of null followed by ".0" + [InlineData(new byte[] { 0x31, 0x2E, 0x30, 0x2E, 0xED, 0xA0, 0x80 })] // "1.0." followed by invalid UTF8 surrogate + public static void Parse_Utf8_InvalidUtf8Bytes_ThrowsFormatException(byte[] invalidUtf8Bytes) + { + Assert.Throws(() => Version.Parse(invalidUtf8Bytes)); + + Assert.False(Version.TryParse(invalidUtf8Bytes, out Version version)); + Assert.Null(version); + } + public static IEnumerable Parse_ValidWithOffsetCount_TestData() { foreach (object[] inputs in Parse_Valid_TestData())