Skip to content

Commit

Permalink
Implement IUtf8SpanParsable on System.Version (#109252)
Browse files Browse the repository at this point in the history
* Implement IUtf8SpanParsable on Version

* Fix utf8 parsing in Version

* Refactor Parse one-liners

* Add documentation
  • Loading branch information
lilinus authored Nov 8, 2024
1 parent cb78461 commit a1b81e1
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 13 deletions.
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

0 comments on commit a1b81e1

Please sign in to comment.