From 810437d650a60d5362e1fe7718cba7e792b04d08 Mon Sep 17 00:00:00 2001 From: lilinus Date: Tue, 30 Jul 2024 12:36:56 +0200 Subject: [PATCH 1/4] Implement IUtf8SpanParsable on Version --- .../src/System/Version.cs | 33 +++++++++++++----- .../System.Runtime/ref/System.Runtime.cs | 6 +++- .../System/VersionTests.cs | 34 +++++++++++++++++++ 3 files changed, 63 insertions(+), 10 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Version.cs b/src/libraries/System.Private.CoreLib/src/System/Version.cs index 6a67dd695f733..cc974a98ed00f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Version.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Version.cs @@ -19,7 +19,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 + public sealed class Version : ICloneable, IComparable, IComparable, IEquatable, ISpanFormattable, IUtf8SpanFormattable, IUtf8SpanParsable { // AssemblyName depends on the order staying the same private readonly int _Major; // Do not rename (binary serialization) @@ -290,6 +290,12 @@ public static Version Parse(string input) public static Version Parse(ReadOnlySpan input) => ParseVersion(input, throwOnFailure: true)!; + static Version IUtf8SpanParsable.Parse(ReadOnlySpan utf8Text, IFormatProvider? provider) => + ParseVersion(utf8Text, throwOnFailure: true)!; + + public static Version Parse(ReadOnlySpan utf8Text) => + ParseVersion(utf8Text, throwOnFailure: true)!; + public static bool TryParse([NotNullWhen(true)] string? input, [NotNullWhen(true)] out Version? result) { if (input == null) @@ -304,10 +310,17 @@ public static bool TryParse([NotNullWhen(true)] string? input, [NotNullWhen(true public static bool TryParse(ReadOnlySpan input, [NotNullWhen(true)] out Version? result) => (result = ParseVersion(input, throwOnFailure: false)) != null; - private static Version? ParseVersion(ReadOnlySpan input, bool throwOnFailure) + public static bool TryParse(ReadOnlySpan utf8Text, [NotNullWhen(true)] out Version? result) => + (result = ParseVersion(utf8Text, throwOnFailure: false)) != null; + + static bool IUtf8SpanParsable.TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, [NotNullWhen(true)] out Version? result) => + (result = ParseVersion(utf8Text, throwOnFailure: false)) != null; + + private static Version? ParseVersion(ReadOnlySpan input, bool throwOnFailure) + where TChar : unmanaged, IUtfChar { // Find the separator between major and minor. It must exist. - int majorEnd = input.IndexOf('.'); + int majorEnd = input.IndexOf(TChar.CastFrom('.')); if (majorEnd < 0) { if (throwOnFailure) throw new ArgumentException(SR.Arg_VersionString, nameof(input)); @@ -317,15 +330,15 @@ public static bool TryParse(ReadOnlySpan input, [NotNullWhen(true)] out Ve // Find the ends of the optional minor and build portions. // We musn't have any separators after build. int buildEnd = -1; - int minorEnd = input.Slice(majorEnd + 1).IndexOf('.'); + int minorEnd = input.Slice(majorEnd + 1).IndexOf(TChar.CastFrom('.')); if (minorEnd >= 0) { minorEnd += (majorEnd + 1); - buildEnd = input.Slice(minorEnd + 1).IndexOf('.'); + buildEnd = input.Slice(minorEnd + 1).IndexOf(TChar.CastFrom('.')); if (buildEnd >= 0) { buildEnd += (minorEnd + 1); - if (input.Slice(buildEnd + 1).Contains('.')) + if (input.Slice(buildEnd + 1).Contains(TChar.CastFrom('.'))) { if (throwOnFailure) throw new ArgumentException(SR.Arg_VersionString, nameof(input)); return null; @@ -375,16 +388,18 @@ public static bool TryParse(ReadOnlySpan input, [NotNullWhen(true)] out Ve } } - private static bool TryParseComponent(ReadOnlySpan component, string componentName, bool throwOnFailure, out int parsedComponent) + private static bool TryParseComponent(ReadOnlySpan component, string componentName, bool throwOnFailure, out int parsedComponent) + where TChar : unmanaged, IUtfChar { if (throwOnFailure) { - parsedComponent = int.Parse(component, NumberStyles.Integer, CultureInfo.InvariantCulture); + parsedComponent = Number.ParseBinaryInteger(component, NumberStyles.Integer, NumberFormatInfo.GetInstance(CultureInfo.InvariantCulture)); ArgumentOutOfRangeException.ThrowIfNegative(parsedComponent, componentName); return true; } - return int.TryParse(component, NumberStyles.Integer, CultureInfo.InvariantCulture, out parsedComponent) && parsedComponent >= 0; + Number.ParsingStatus parseStatus = Number.TryParseBinaryIntegerStyle(component, NumberStyles.Integer, NumberFormatInfo.GetInstance(CultureInfo.InvariantCulture), out parsedComponent); + return parseStatus == Number.ParsingStatus.OK && parsedComponent >= 0; } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index db49bbfb30de2..713b694b29de8 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -7743,7 +7743,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 + public sealed partial class Version : System.ICloneable, System.IComparable, System.IComparable, System.IEquatable, System.IFormattable, System.ISpanFormattable, System.IUtf8SpanFormattable, System.IUtf8SpanParsable { public Version() { } public Version(int major, int minor) { } @@ -7768,17 +7768,21 @@ public Version(string version) { } public static bool operator !=(System.Version? v1, System.Version? v2) { throw null; } public static bool operator <(System.Version? v1, System.Version? v2) { throw null; } public static bool operator <=(System.Version? v1, System.Version? v2) { throw null; } + public static System.Version Parse(System.ReadOnlySpan utf8Text) { throw null; } 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; } bool System.ISpanFormattable.TryFormat(System.Span destination, out int charsWritten, System.ReadOnlySpan format, System.IFormatProvider? provider) { 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; } public override string ToString() { throw null; } public string ToString(int fieldCount) { throw null; } public bool TryFormat(System.Span utf8Destination, int fieldCount, out int bytesWritten) { throw null; } public bool TryFormat(System.Span utf8Destination, out int bytesWritten) { throw null; } public bool TryFormat(System.Span destination, int fieldCount, out int charsWritten) { throw null; } public bool TryFormat(System.Span destination, out int charsWritten) { throw null; } + public static bool TryParse(System.ReadOnlySpan utf8Text, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Version? result) { throw null; } public static bool TryParse(System.ReadOnlySpan input, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Version? result) { throw null; } public static bool TryParse([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? input, [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 e29052ae6b8a8..4bcb5fc5c41c2 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 @@ -311,6 +311,23 @@ public static void Parse_Span_ValidInput_ReturnsExpected(string input, int offse Assert.Equal(expected, version); } + [Theory] + [MemberData(nameof(Parse_ValidWithOffsetCount_TestData))] + public static void Parse_Utf8_ValidInput_ReturnsExpected(string input, int offset, int count, Version expected) + { + if (input == null) + { + return; + } + + byte[] utf8Bytes = Encoding.UTF8.GetBytes(input.Substring(offset, count)); + + Assert.Equal(expected, Version.Parse(utf8Bytes)); + + Assert.True(Version.TryParse(utf8Bytes, out Version version)); + Assert.Equal(expected, version); + } + [Theory] [MemberData(nameof(Parse_Invalid_TestData))] public static void Parse_Span_InvalidInput_ThrowsException(string input, Type exceptionType) @@ -326,6 +343,23 @@ public static void Parse_Span_InvalidInput_ThrowsException(string input, Type ex Assert.Null(version); } + [Theory] + [MemberData(nameof(Parse_Invalid_TestData))] + public static void Parse_Utf8_InvalidInput_ThrowsException(string input, Type exceptionType) + { + if (input == null) + { + return; + } + + byte[] utf8Bytes = Encoding.UTF8.GetBytes(input); + + Assert.Throws(exceptionType, () => Version.Parse(utf8Bytes)); + + Assert.False(Version.TryParse(utf8Bytes, out Version version)); + Assert.Null(version); + } + public static IEnumerable ToString_TestData() { yield return new object[] { new Version(1, 2), new string[] { "", "1", "1.2" } }; From d0da5054aa5de993cbaa12df2e86b1053d1d60d6 Mon Sep 17 00:00:00 2001 From: lilinus Date: Thu, 1 Aug 2024 13:31:24 +0200 Subject: [PATCH 2/4] Fix utf8 parsing in Version --- .../src/System/Version.cs | 18 ++++++++++++++---- .../System.Runtime/ref/System.Runtime.cs | 4 ++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Version.cs b/src/libraries/System.Private.CoreLib/src/System/Version.cs index cc974a98ed00f..e2a49a609a44b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Version.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Version.cs @@ -290,8 +290,17 @@ public static Version Parse(string input) public static Version Parse(ReadOnlySpan input) => ParseVersion(input, throwOnFailure: true)!; - static Version IUtf8SpanParsable.Parse(ReadOnlySpan utf8Text, IFormatProvider? provider) => - ParseVersion(utf8Text, throwOnFailure: true)!; + /// + static Version IUtf8SpanParsable.Parse(ReadOnlySpan utf8Text, IFormatProvider? provider) + { + Version? result = ParseVersion(utf8Text, throwOnFailure: false); + // Required to throw FormatException for invalid input according to contract. + if (result == null) + { + ThrowHelper.ThrowFormatInvalidString(); + } + return result; + } public static Version Parse(ReadOnlySpan utf8Text) => ParseVersion(utf8Text, throwOnFailure: true)!; @@ -313,6 +322,7 @@ public static bool TryParse(ReadOnlySpan input, [NotNullWhen(true)] out Ve public static bool TryParse(ReadOnlySpan utf8Text, [NotNullWhen(true)] out Version? result) => (result = ParseVersion(utf8Text, throwOnFailure: false)) != null; + /// static bool IUtf8SpanParsable.TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, [NotNullWhen(true)] out Version? result) => (result = ParseVersion(utf8Text, throwOnFailure: false)) != null; @@ -393,12 +403,12 @@ private static bool TryParseComponent(ReadOnlySpan component, stri { if (throwOnFailure) { - parsedComponent = Number.ParseBinaryInteger(component, NumberStyles.Integer, NumberFormatInfo.GetInstance(CultureInfo.InvariantCulture)); + parsedComponent = Number.ParseBinaryInteger(component, NumberStyles.Integer, NumberFormatInfo.InvariantInfo); ArgumentOutOfRangeException.ThrowIfNegative(parsedComponent, componentName); return true; } - Number.ParsingStatus parseStatus = Number.TryParseBinaryIntegerStyle(component, NumberStyles.Integer, NumberFormatInfo.GetInstance(CultureInfo.InvariantCulture), out parsedComponent); + Number.ParsingStatus parseStatus = Number.TryParseBinaryIntegerStyle(component, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out parsedComponent); return parseStatus == Number.ParsingStatus.OK && parsedComponent >= 0; } diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 713b694b29de8..1d8f362398ef4 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -7774,8 +7774,8 @@ public Version(string version) { } string System.IFormattable.ToString(string? format, System.IFormatProvider? formatProvider) { throw null; } bool System.ISpanFormattable.TryFormat(System.Span destination, out int charsWritten, System.ReadOnlySpan format, System.IFormatProvider? provider) { 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; } + 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; } public override string ToString() { throw null; } public string ToString(int fieldCount) { throw null; } public bool TryFormat(System.Span utf8Destination, int fieldCount, out int bytesWritten) { throw null; } From 17e0c187885519a06edd492e718435f429092bb2 Mon Sep 17 00:00:00 2001 From: lilinus Date: Wed, 30 Oct 2024 08:17:11 +0100 Subject: [PATCH 3/4] Refactor Parse one-liners --- .../src/System/Version.cs | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Version.cs b/src/libraries/System.Private.CoreLib/src/System/Version.cs index e2a49a609a44b..5268e3052b369 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Version.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Version.cs @@ -313,18 +313,28 @@ public static bool TryParse([NotNullWhen(true)] string? input, [NotNullWhen(true return false; } - return (result = ParseVersion(input.AsSpan(), throwOnFailure: false)) != null; + result = ParseVersion(input.AsSpan(), throwOnFailure: false); + return result is not null; } - public static bool TryParse(ReadOnlySpan input, [NotNullWhen(true)] out Version? result) => - (result = ParseVersion(input, throwOnFailure: false)) != null; + public static bool TryParse(ReadOnlySpan input, [NotNullWhen(true)] out Version? result) + { + result = ParseVersion(input, throwOnFailure: false); + return result is not null; + } - public static bool TryParse(ReadOnlySpan utf8Text, [NotNullWhen(true)] out Version? result) => - (result = ParseVersion(utf8Text, throwOnFailure: false)) != null; + public static bool TryParse(ReadOnlySpan utf8Text, [NotNullWhen(true)] out Version? result) + { + result = ParseVersion(utf8Text, throwOnFailure: false); + return result is not null; + } /// - static bool IUtf8SpanParsable.TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, [NotNullWhen(true)] out Version? result) => - (result = ParseVersion(utf8Text, throwOnFailure: false)) != null; + static bool IUtf8SpanParsable.TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, [NotNullWhen(true)] out Version? result) + { + result = ParseVersion(utf8Text, throwOnFailure: false); + return result is not null; + } private static Version? ParseVersion(ReadOnlySpan input, bool throwOnFailure) where TChar : unmanaged, IUtfChar From 8b5cd9c1dee1dec6a0c9e77dfceca669e71f636a Mon Sep 17 00:00:00 2001 From: lilinus Date: Wed, 30 Oct 2024 08:30:28 +0100 Subject: [PATCH 4/4] Add documentation --- .../src/System/Version.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/libraries/System.Private.CoreLib/src/System/Version.cs b/src/libraries/System.Private.CoreLib/src/System/Version.cs index 5268e3052b369..9aeac15161b87 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Version.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Version.cs @@ -302,6 +302,15 @@ static Version IUtf8SpanParsable.Parse(ReadOnlySpan utf8Text, IFo return result; } + /// + /// Converts the specified read-only span of UTF-8 characters that represents a version number to an equivalent Version object. + /// + /// A read-only span of UTF-8 characters that contains a version number to convert. + /// An object that is equivalent to the version number specified in the parameter. + /// has fewer than two or more than four version components. + /// At least one component in is less than zero. + /// At least one component in is not an integer. + /// At least one component in represents a number that is greater than . public static Version Parse(ReadOnlySpan utf8Text) => ParseVersion(utf8Text, throwOnFailure: true)!; @@ -323,6 +332,15 @@ public static bool TryParse(ReadOnlySpan input, [NotNullWhen(true)] out Ve return result is not null; } + /// + /// 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. + /// + /// The span of UTF-8 characters to parse. + /// + /// When this method returns, contains the Version equivalent of the number that is contained in , if the conversion succeeded. + /// If is empty, or if the conversion fails, result is null when the method returns. + /// + /// true if the parameter was converted successfully; otherwise, false. public static bool TryParse(ReadOnlySpan utf8Text, [NotNullWhen(true)] out Version? result) { result = ParseVersion(utf8Text, throwOnFailure: false);