Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement IUtf8SpanParsable on System.Version #109252

Merged
merged 6 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
77 changes: 65 additions & 12 deletions src/libraries/System.Private.CoreLib/src/System/Version.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Version?>, IEquatable<Version?>, ISpanFormattable, IUtf8SpanFormattable
public sealed class Version : ICloneable, IComparable, IComparable<Version?>, IEquatable<Version?>, ISpanFormattable, IUtf8SpanFormattable, IUtf8SpanParsable<Version>
{
// AssemblyName depends on the order staying the same
private readonly int _Major; // Do not rename (binary serialization)
Expand Down Expand Up @@ -290,6 +290,30 @@ public static Version Parse(string input)
public static Version Parse(ReadOnlySpan<char> input) =>
ParseVersion(input, throwOnFailure: true)!;

/// <inheritdoc cref="IUtf8SpanParsable{TSelf}.Parse(ReadOnlySpan{byte}, IFormatProvider?)"/>
static Version IUtf8SpanParsable<Version>.Parse(ReadOnlySpan<byte> 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;
}

/// <summary>
/// Converts the specified read-only span of UTF-8 characters that represents a version number to an equivalent Version object.
/// </summary>
/// <param name="utf8Text">A read-only span of UTF-8 characters that contains a version number to convert.</param>
/// <returns>An object that is equivalent to the version number specified in the <paramref name="utf8Text" /> parameter.</returns>
/// <exception cref="ArgumentException"><paramref name="utf8Text" /> has fewer than two or more than four version components.</exception>
/// <exception cref="ArgumentOutOfRangeException">At least one component in <paramref name="utf8Text" /> is less than zero.</exception>
/// <exception cref="FormatException">At least one component in <paramref name="utf8Text" /> is not an integer.</exception>
/// <exception cref="OverflowException">At least one component in <paramref name="utf8Text" /> represents a number that is greater than <see cref="int.MaxValue"/>.</exception>
public static Version Parse(ReadOnlySpan<byte> utf8Text) =>
ParseVersion(utf8Text, throwOnFailure: true)!;

public static bool TryParse([NotNullWhen(true)] string? input, [NotNullWhen(true)] out Version? result)
{
if (input == null)
Expand All @@ -298,16 +322,43 @@ 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<char> input, [NotNullWhen(true)] out Version? result)
{
result = ParseVersion(input, throwOnFailure: false);
return result is not null;
}

public static bool TryParse(ReadOnlySpan<char> input, [NotNullWhen(true)] out Version? result) =>
(result = ParseVersion(input, throwOnFailure: false)) != null;
/// <summary>
/// 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.
/// </summary>
/// <param name="utf8Text">The span of UTF-8 characters to parse.</param>
/// <param name="result">
/// When this method returns, contains the Version equivalent of the number that is contained in <paramref name="utf8Text" />, if the conversion succeeded.
/// If <paramref name="utf8Text" /> is empty, or if the conversion fails, result is null when the method returns.
/// </param>
/// <returns>true if the <paramref name="utf8Text" /> parameter was converted successfully; otherwise, false.</returns>
public static bool TryParse(ReadOnlySpan<byte> utf8Text, [NotNullWhen(true)] out Version? result)
{
result = ParseVersion(utf8Text, throwOnFailure: false);
return result is not null;
}

/// <inheritdoc cref="IUtf8SpanParsable{TSelf}.TryParse(ReadOnlySpan{byte}, IFormatProvider?, out TSelf)"/>
static bool IUtf8SpanParsable<Version>.TryParse(ReadOnlySpan<byte> utf8Text, IFormatProvider? provider, [NotNullWhen(true)] out Version? result)
{
result = ParseVersion(utf8Text, throwOnFailure: false);
return result is not null;
}

private static Version? ParseVersion(ReadOnlySpan<char> input, bool throwOnFailure)
private static Version? ParseVersion<TChar>(ReadOnlySpan<TChar> input, bool throwOnFailure)
where TChar : unmanaged, IUtfChar<TChar>
{
// 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));
Expand All @@ -317,15 +368,15 @@ public static bool TryParse(ReadOnlySpan<char> 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;
Expand Down Expand Up @@ -375,16 +426,18 @@ public static bool TryParse(ReadOnlySpan<char> input, [NotNullWhen(true)] out Ve
}
}

private static bool TryParseComponent(ReadOnlySpan<char> component, string componentName, bool throwOnFailure, out int parsedComponent)
private static bool TryParseComponent<TChar>(ReadOnlySpan<TChar> component, string componentName, bool throwOnFailure, out int parsedComponent)
where TChar : unmanaged, IUtfChar<TChar>
{
if (throwOnFailure)
{
parsedComponent = int.Parse(component, NumberStyles.Integer, CultureInfo.InvariantCulture);
parsedComponent = Number.ParseBinaryInteger<TChar, int>(component, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
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.InvariantInfo, out parsedComponent);
return parseStatus == Number.ParsingStatus.OK && parsedComponent >= 0;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
Expand Down
6 changes: 5 additions & 1 deletion src/libraries/System.Runtime/ref/System.Runtime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7740,7 +7740,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.Version?>, System.IEquatable<System.Version?>, System.IFormattable, System.ISpanFormattable, System.IUtf8SpanFormattable
public sealed partial class Version : System.ICloneable, System.IComparable, System.IComparable<System.Version?>, System.IEquatable<System.Version?>, System.IFormattable, System.ISpanFormattable, System.IUtf8SpanFormattable, System.IUtf8SpanParsable<System.Version>
{
public Version() { }
public Version(int major, int minor) { }
Expand All @@ -7765,17 +7765,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<byte> utf8Text) { throw null; }
public static System.Version Parse(System.ReadOnlySpan<char> 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<char> destination, out int charsWritten, System.ReadOnlySpan<char> format, System.IFormatProvider? provider) { throw null; }
bool System.IUtf8SpanFormattable.TryFormat(System.Span<byte> utf8Destination, out int bytesWritten, System.ReadOnlySpan<char> format, System.IFormatProvider? provider) { throw null; }
static System.Version System.IUtf8SpanParsable<System.Version>.Parse(System.ReadOnlySpan<byte> utf8Text, System.IFormatProvider? provider) { throw null; }
static bool System.IUtf8SpanParsable<System.Version>.TryParse(System.ReadOnlySpan<byte> 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<byte> utf8Destination, int fieldCount, out int bytesWritten) { throw null; }
public bool TryFormat(System.Span<byte> utf8Destination, out int bytesWritten) { throw null; }
public bool TryFormat(System.Span<char> destination, int fieldCount, out int charsWritten) { throw null; }
public bool TryFormat(System.Span<char> destination, out int charsWritten) { throw null; }
public static bool TryParse(System.ReadOnlySpan<byte> utf8Text, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Version? result) { throw null; }
public static bool TryParse(System.ReadOnlySpan<char> 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; }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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<object[]> ToString_TestData()
{
yield return new object[] { new Version(1, 2), new string[] { "", "1", "1.2" } };
Expand Down