diff --git a/src/libraries/System.Private.CoreLib/src/System/Version.cs b/src/libraries/System.Private.CoreLib/src/System/Version.cs index 9b8cf0ba33b560..6d0641f8ce4807 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Version.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Version.cs @@ -20,7 +20,7 @@ namespace System [Serializable] [TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")] - public sealed class Version : ICloneable, IComparable, IComparable, IEquatable, ISpanFormattable, IUtf8SpanFormattable, IUtf8SpanParsable + public sealed class Version : ICloneable, IComparable, IComparable, IEquatable, ISpanFormattable, IUtf8SpanFormattable, IUtf8SpanParsable, ISpanParsable { // AssemblyName depends on the order staying the same private readonly int _Major; // Do not rename (binary serialization) @@ -288,13 +288,28 @@ public static Version Parse(string input) return ParseVersion(input.AsSpan(), throwOnFailure: true)!; } + /// + static Version IParsable.Parse(string s, IFormatProvider? provider) + { + ArgumentNullException.ThrowIfNull(s); + return ParseHelper(s.AsSpan()); + } + public static Version Parse(ReadOnlySpan input) => ParseVersion(input, throwOnFailure: true)!; + /// + static Version ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) => + ParseHelper(s); + /// - static Version IUtf8SpanParsable.Parse(ReadOnlySpan utf8Text, IFormatProvider? provider) + static Version IUtf8SpanParsable.Parse(ReadOnlySpan utf8Text, IFormatProvider? provider) => + ParseHelper(utf8Text); + + private static Version ParseHelper(ReadOnlySpan input) + where TChar : unmanaged, IUtfChar { - Version? result = ParseVersion(utf8Text, throwOnFailure: false); + Version? result = ParseVersion(input, throwOnFailure: false); // Required to throw FormatException for invalid input according to contract. if (result == null) { @@ -327,12 +342,20 @@ public static bool TryParse([NotNullWhen(true)] string? input, [NotNullWhen(true return result is not null; } + /// + static bool IParsable.TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [NotNullWhen(true)] out Version? result) => + TryParse(s, out result); + public static bool TryParse(ReadOnlySpan input, [NotNullWhen(true)] out Version? result) { result = ParseVersion(input, throwOnFailure: false); return result is not null; } + /// + static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, [NotNullWhen(true)] out Version? result) => + TryParse(s, out result); + /// /// Tries to convert the UTF-8 representation of a version number to an equivalent Version object, and returns a value that indicates whether the conversion succeeded. /// diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 7b2fd5e2ea9641..7e0d35116f8e67 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -7895,7 +7895,7 @@ protected ValueType() { } public override int GetHashCode() { throw null; } public override string? ToString() { throw null; } } - public sealed partial class Version : System.ICloneable, System.IComparable, System.IComparable, System.IEquatable, System.IFormattable, System.ISpanFormattable, System.IUtf8SpanFormattable, System.IUtf8SpanParsable + public sealed partial class Version : System.ICloneable, System.IComparable, System.IComparable, System.IEquatable, System.IFormattable, System.IParsable, System.ISpanFormattable, System.ISpanParsable, System.IUtf8SpanFormattable, System.IUtf8SpanParsable { public Version() { } public Version(int major, int minor) { } @@ -7924,7 +7924,11 @@ public Version(string version) { } public static System.Version Parse(System.ReadOnlySpan input) { throw null; } public static System.Version Parse(string input) { throw null; } string System.IFormattable.ToString(string? format, System.IFormatProvider? formatProvider) { throw null; } + static System.Version System.IParsable.Parse(string s, System.IFormatProvider? provider) { throw null; } + static bool System.IParsable.TryParse([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? s, System.IFormatProvider? provider, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Version result) { throw null; } bool System.ISpanFormattable.TryFormat(System.Span destination, out int charsWritten, System.ReadOnlySpan format, System.IFormatProvider? provider) { throw null; } + static System.Version System.ISpanParsable.Parse(System.ReadOnlySpan s, System.IFormatProvider? provider) { throw null; } + static bool System.ISpanParsable.TryParse(System.ReadOnlySpan s, System.IFormatProvider? provider, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Version result) { throw null; } bool System.IUtf8SpanFormattable.TryFormat(System.Span utf8Destination, out int bytesWritten, System.ReadOnlySpan format, System.IFormatProvider? provider) { throw null; } static System.Version System.IUtf8SpanParsable.Parse(System.ReadOnlySpan utf8Text, System.IFormatProvider? provider) { throw null; } static bool System.IUtf8SpanParsable.TryParse(System.ReadOnlySpan utf8Text, System.IFormatProvider? provider, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Version result) { throw null; } 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 6a9a088341c9fa..7c949ee622a72a 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 @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Globalization; using System.Text; using Xunit; @@ -502,5 +503,124 @@ public static void TryFormat_Invoke_WritesExpected(Version version, string[] exp AssertExtensions.Throws("fieldCount", () => version.TryFormat(dest, maxFieldCount + 1, out bytesWritten)); // Index > version.fieldCount } } + + public static IEnumerable Parse_Invalid_NonNull_TestData() + { + foreach (object[] data in Parse_Invalid_TestData()) + { + if (data[0] != null) + { + yield return new object[] { data[0] }; + } + } + } + + [Theory] + [MemberData(nameof(Parse_Valid_TestData))] + public static void IParsable_Parse_ValidInput_ReturnsExpected(string input, Version expected) + { + Assert.Equal(expected, ParsableHelper.Parse(input, null)); + Assert.Equal(expected, ParsableHelper.Parse(input, CultureInfo.InvariantCulture)); + + Assert.True(ParsableHelper.TryParse(input, null, out Version? result)); + Assert.Equal(expected, result); + Assert.True(ParsableHelper.TryParse(input, CultureInfo.InvariantCulture, out result)); + Assert.Equal(expected, result); + } + + [Theory] + [MemberData(nameof(Parse_Valid_TestData))] + public static void ISpanParsable_Parse_ValidInput_ReturnsExpected(string input, Version expected) + { + Assert.Equal(expected, SpanParsableHelper.Parse(input.AsSpan(), null)); + Assert.Equal(expected, SpanParsableHelper.Parse(input.AsSpan(), CultureInfo.InvariantCulture)); + + Assert.True(SpanParsableHelper.TryParse(input.AsSpan(), null, out Version? result)); + Assert.Equal(expected, result); + Assert.True(SpanParsableHelper.TryParse(input.AsSpan(), CultureInfo.InvariantCulture, out result)); + Assert.Equal(expected, result); + } + + [Theory] + [MemberData(nameof(Parse_Valid_TestData))] + public static void IUtf8SpanParsable_Parse_ValidInput_ReturnsExpected(string input, Version expected) + { + byte[] utf8 = Encoding.UTF8.GetBytes(input); + Assert.Equal(expected, Utf8SpanParsableHelper.Parse(utf8, null)); + Assert.Equal(expected, Utf8SpanParsableHelper.Parse(utf8, CultureInfo.InvariantCulture)); + + Assert.True(Utf8SpanParsableHelper.TryParse(utf8, null, out Version? result)); + Assert.Equal(expected, result); + Assert.True(Utf8SpanParsableHelper.TryParse(utf8, CultureInfo.InvariantCulture, out result)); + Assert.Equal(expected, result); + } + + [Fact] + public static void IParsable_Parse_NullInput_ThrowsArgumentNullException() + { + AssertExtensions.Throws("s", () => ParsableHelper.Parse(null!, null)); + AssertExtensions.Throws("s", () => ParsableHelper.Parse(null!, CultureInfo.InvariantCulture)); + + Assert.False(ParsableHelper.TryParse(null, null, out Version? result)); + Assert.Null(result); + Assert.False(ParsableHelper.TryParse(null, CultureInfo.InvariantCulture, out result)); + Assert.Null(result); + } + + [Theory] + [MemberData(nameof(Parse_Invalid_NonNull_TestData))] + public static void IParsable_Parse_InvalidInput_ThrowsFormatException(string input) + { + Assert.Throws(() => ParsableHelper.Parse(input, null)); + Assert.Throws(() => ParsableHelper.Parse(input, CultureInfo.InvariantCulture)); + + Assert.False(ParsableHelper.TryParse(input, null, out Version? result)); + Assert.Null(result); + Assert.False(ParsableHelper.TryParse(input, CultureInfo.InvariantCulture, out result)); + Assert.Null(result); + } + + [Theory] + [MemberData(nameof(Parse_Invalid_NonNull_TestData))] + public static void ISpanParsable_Parse_InvalidInput_ThrowsFormatException(string input) + { + Assert.Throws(() => SpanParsableHelper.Parse(input.AsSpan(), null)); + Assert.Throws(() => SpanParsableHelper.Parse(input.AsSpan(), CultureInfo.InvariantCulture)); + + Assert.False(SpanParsableHelper.TryParse(input.AsSpan(), null, out Version? result)); + Assert.Null(result); + Assert.False(SpanParsableHelper.TryParse(input.AsSpan(), CultureInfo.InvariantCulture, out result)); + Assert.Null(result); + } + + [Theory] + [MemberData(nameof(Parse_Invalid_NonNull_TestData))] + public static void IUtf8SpanParsable_Parse_InvalidInput_ThrowsFormatException(string input) + { + byte[] utf8 = Encoding.UTF8.GetBytes(input); + Assert.Throws(() => Utf8SpanParsableHelper.Parse(utf8, null)); + Assert.Throws(() => Utf8SpanParsableHelper.Parse(utf8, CultureInfo.InvariantCulture)); + + Assert.False(Utf8SpanParsableHelper.TryParse(utf8, null, out Version? result)); + Assert.Null(result); + Assert.False(Utf8SpanParsableHelper.TryParse(utf8, CultureInfo.InvariantCulture, out result)); + Assert.Null(result); + } + + [Theory] + [InlineData(".")] + [InlineData("1.")] + [InlineData("1.0.")] + [InlineData("1.0.0.")] + public static void IParsable_ISpanParsable_Parse_TrailingDot_ThrowsFormatException(string input) + { + Assert.Throws(() => ParsableHelper.Parse(input, null)); + Assert.Throws(() => SpanParsableHelper.Parse(input.AsSpan(), null)); + + Assert.False(ParsableHelper.TryParse(input, null, out Version? result)); + Assert.Null(result); + Assert.False(SpanParsableHelper.TryParse(input.AsSpan(), null, out result)); + Assert.Null(result); + } } }