Skip to content
Open
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
46 changes: 34 additions & 12 deletions src/libraries/System.Private.CoreLib/src/System/Version.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;

namespace System
{
Expand Down Expand Up @@ -387,15 +388,15 @@ static bool IUtf8SpanParsable<Version>.TryParse(ReadOnlySpan<byte> 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;
}

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;
}
Expand All @@ -404,40 +405,61 @@ static bool IUtf8SpanParsable<Version>.TryParse(ReadOnlySpan<byte> 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;
}
}
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<TChar>(ReadOnlySpan<TChar> component, string componentName, bool throwOnFailure, out int parsedComponent)
private static bool TryParseComponent<TChar>(ReadOnlySpan<TChar> component, string componentName, bool throwOnFailure, ReadOnlySpan<TChar> originalInput, out int parsedComponent)
where TChar : unmanaged, IUtfChar<TChar>
{
if (throwOnFailure)
Number.ParsingStatus parseStatus = Number.TryParseBinaryIntegerStyle(component, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out parsedComponent);

if (parseStatus == Number.ParsingStatus.OK && parsedComponent >= 0)
{
parsedComponent = Number.ParseBinaryInteger<TChar, int>(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<TChar> originalInput)
{
ArgumentOutOfRangeException.ThrowIfNegative(parsedComponent, componentName);

if (parseStatus == Number.ParsingStatus.Overflow)
{
Number.ThrowOverflowException<int>();
}

string inputString = typeof(TChar) == typeof(char) ?
Unsafe.BitCast<ReadOnlySpan<TChar>, ReadOnlySpan<char>>(originalInput).ToString() :
Encoding.UTF8.GetString(Unsafe.BitCast<ReadOnlySpan<TChar>, ReadOnlySpan<byte>>(originalInput));

throw new FormatException(SR.Format(SR.Format_InvalidStringWithValue, inputString));
}
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<FormatException>(() => 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<FormatException>(() => 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<FormatException>(() => 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<FormatException>(() => Version.Parse(invalidUtf8Bytes));

Assert.False(Version.TryParse(invalidUtf8Bytes, out Version version));
Assert.Null(version);
}

public static IEnumerable<object[]> Parse_ValidWithOffsetCount_TestData()
{
foreach (object[] inputs in Parse_Valid_TestData())
Expand Down
Loading