Skip to content
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
59 changes: 59 additions & 0 deletions src/UniGetUI.Core.Tools.Tests/ToolsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,65 @@ public void TestGetVersionStringAsFloat_WithNonNumericVersion_ReturnsNull()
Assert.Equal(CoreTools.Version.Null, v);
}

[Theory]
[InlineData("37149234619", "38756550835")]
[InlineData("1.2147483648", "1.2147483649")]
[InlineData("20260101.120000000001", "20260101.120000000002")]
[InlineData("99999999999999999999", "100000000000000000000")]
[InlineData("1.20260101120000000000001", "1.20260101120000000000002")]
public void TestGetVersionStringAsFloat_WithSegmentsBeyondInt32_OrdersCorrectly(
string older,
string newer
)
{
Assert.True(
CoreTools.VersionStringToStruct(older) < CoreTools.VersionStringToStruct(newer)
);
}

[Theory]
[InlineData("1.0.0+20260101123456", 1, 0, 0)]
[InlineData("1.0 (20260101123456)", 1, 0, 0)]
[InlineData("2.0.0+42", 2, 0, 42)]
[InlineData("1.0.0+2147483647", 1, 0, 2147483647)]
[InlineData("1.0.0+2147483648", 1, 0, 0)]
public void TestGetVersionStringAsFloat_WithSplicedDigitRuns_KeepsInt32Limit(
string version,
int major,
int minor,
int patch
)
{
Assert.Equal(
new CoreTools.Version(major, minor, patch),
CoreTools.VersionStringToStruct(version)
);
}

[Fact]
public void TestGetVersionStringAsFloat_WithExtraSegmentBeyondInt32_HashesConsistently()
{
CoreTools.Version a = CoreTools.VersionStringToStruct("1.2.3.4.37149234619");
CoreTools.Version b = CoreTools.VersionStringToStruct("1.2.3.4.37149234619.0");

Assert.Equal(a, b);
Assert.Equal(a.GetHashCode(), b.GetHashCode());
Assert.NotEqual(a, CoreTools.VersionStringToStruct("1.2.3.4.38756550835"));
}

[Theory]
[InlineData("20250301123456", "2.0.0")]
[InlineData("37149234619", "1.0.0")]
public void TestGetVersionStringAsFloat_WithLongNumericScheme_RanksAboveDottedVersions(
string numericScheme,
string dotted
)
{
Assert.True(
CoreTools.VersionStringToStruct(numericScheme) > CoreTools.VersionStringToStruct(dotted)
);
}

// An underscore introducing a pre-release tag rather than a numeric revision must keep
// reporting as unknown, as it did before '_' became a segment separator. Parsing these
// would rank the pre-release above the final release and hide the 2.0.0_rc1 -> 2.0.0
Expand Down
91 changes: 66 additions & 25 deletions src/UniGetUI.Core.Tools/Tools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Globalization;
using System.Net;
using System.Net.NetworkInformation;
using System.Numerics;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Security.Principal;
Expand Down Expand Up @@ -534,10 +535,10 @@
{
public static readonly Version Null = new(-1, -1, -1, -1);

public readonly int Major;
public readonly int Minor;
public readonly int Patch;
public readonly int Remainder;
public readonly BigInteger Major;
public readonly BigInteger Minor;
public readonly BigInteger Patch;
public readonly BigInteger Remainder;

/// <summary>
/// Segments past the fourth, with trailing zeroes trimmed so that equal versions
Expand All @@ -546,12 +547,23 @@
/// (e.g. a build revision on a four-part version: 1.2.3.4_1 vs 1.2.3.4_2) order
/// correctly instead of collapsing into Remainder.
/// </summary>
private readonly int[]? _extraSegments;
private readonly BigInteger[]? _extraSegments;
Comment thread
GabrielDuf marked this conversation as resolved.
Resolved

public Version(int major, int minor = 0, int patch = 0, int remainder = 0)
public Version(
BigInteger major,
BigInteger minor = default,
BigInteger patch = default,
BigInteger remainder = default
)
: this(major, minor, patch, remainder, null) { }

private Version(int major, int minor, int patch, int remainder, int[]? extraSegments)
private Version(
BigInteger major,
BigInteger minor,
BigInteger patch,
BigInteger remainder,
BigInteger[]? extraSegments
)
{
Major = major;
Minor = minor;
Expand All @@ -564,16 +576,16 @@
/// Builds a Version from an arbitrary number of segments. Segments past the fourth are
/// retained for comparison rather than being folded into Remainder.
/// </summary>
internal static Version FromSegments(IReadOnlyList<int> segments)
internal static Version FromSegments(IReadOnlyList<BigInteger> segments)
{
int count = segments.Count;
while (count > 4 && segments[count - 1] == 0)
count--;

int[]? extra = null;
BigInteger[]? extra = null;
if (count > 4)
{
extra = new int[count - 4];
extra = new BigInteger[count - 4];
for (int i = 4; i < count; i++)
extra[i - 4] = segments[i];
}
Expand All @@ -586,12 +598,14 @@
extra
);

static int Segment(IReadOnlyList<int> values, int index) =>
index < values.Count ? values[index] : 0;
static BigInteger Segment(IReadOnlyList<BigInteger> values, int index) =>
index < values.Count ? values[index] : BigInteger.Zero;
}

private int ExtraSegment(int index) =>
_extraSegments is not null && index < _extraSegments.Length ? _extraSegments[index] : 0;
private BigInteger ExtraSegment(int index) =>
_extraSegments is not null && index < _extraSegments.Length
? _extraSegments[index]
: BigInteger.Zero;

public int CompareTo(object? other_) => other_ is Version other ? CompareTo(other) : 0;

Expand Down Expand Up @@ -669,7 +683,7 @@
hash.Add(Minor);
hash.Add(Patch);
hash.Add(Remainder);
foreach (int segment in _extraSegments ?? [])
foreach (BigInteger segment in _extraSegments ?? [])
hash.Add(segment);
return hash.ToHashCode();
}
Expand Down Expand Up @@ -732,25 +746,34 @@
// Collect every segment rather than capping at four. Capping used to append the
// surplus digits to the fourth segment, which inflated it: 1.2.3.4_1 became
// 1.2.3.41 and so compared as greater than the newer 1.2.3.5, hiding the update.
List<string> segmentDigits = [""];
List<BigInteger> numbers = [];
StringBuilder digits = new();
bool gapInSegment = false;
bool spliced = false;
bool first = true;

foreach (char c in version)
{
if (char.IsDigit(c))
segmentDigits[^1] += c;
{
spliced |= gapInSegment;
digits.Append(c);
}
else if (!first && separators.Contains(c))
segmentDigits.Add("");
{
numbers.Add(ParseVersionSegment(digits.ToString(), spliced));
digits.Clear();
gapInSegment = false;
spliced = false;
}
else if (digits.Length > 0)
{
gapInSegment = true;
}
first = false;
}

int[] numbers = new int[segmentDigits.Count];
for (int i = 0; i < numbers.Length; i++)
{
if (int.TryParse(segmentDigits[i], out int val))
numbers[i] = val;
}

numbers.Add(ParseVersionSegment(digits.ToString(), spliced));
return Version.FromSegments(numbers);
}
catch
Expand All @@ -760,6 +783,24 @@
}
}

private static BigInteger ParseVersionSegment(string digits, bool spliced)
{
if (spliced)
return int.TryParse(digits, out int small) ? small : BigInteger.Zero;

if (long.TryParse(digits, out long value))
return value;

return BigInteger.TryParse(
digits,
NumberStyles.None,
CultureInfo.InvariantCulture,
out BigInteger large
)
? large
: BigInteger.Zero;
}

public static System.Version NormalizeVersionForComparison(System.Version version)
{
return version.Revision >= 0
Expand Down Expand Up @@ -1985,7 +2026,7 @@
}

Task reads = Task.WhenAll(stdout, stderr);
reads.ContinueWith(

Check warning on line 2029 in src/UniGetUI.Core.Tools/Tools.cs

View workflow job for this annotation

GitHub Actions / Linux (Avalonia)

Do not create tasks without passing a TaskScheduler (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2008)

Check warning on line 2029 in src/UniGetUI.Core.Tools/Tools.cs

View workflow job for this annotation

GitHub Actions / Linux (NativeAOT)

Do not create tasks without passing a TaskScheduler (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2008)

Check warning on line 2029 in src/UniGetUI.Core.Tools/Tools.cs

View workflow job for this annotation

GitHub Actions / Windows (Avalonia)

Do not create tasks without passing a TaskScheduler (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2008)

Check warning on line 2029 in src/UniGetUI.Core.Tools/Tools.cs

View workflow job for this annotation

GitHub Actions / Windows (NativeAOT)

Do not create tasks without passing a TaskScheduler (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2008)

Check warning on line 2029 in src/UniGetUI.Core.Tools/Tools.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Do not create tasks without passing a TaskScheduler (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2008)

Check warning on line 2029 in src/UniGetUI.Core.Tools/Tools.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Do not create tasks without passing a TaskScheduler (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2008)

Check warning on line 2029 in src/UniGetUI.Core.Tools/Tools.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Do not create tasks without passing a TaskScheduler (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2008)
completed => _ = completed.Exception,
TaskContinuationOptions.OnlyOnFaulted
);
Expand Down
4 changes: 4 additions & 0 deletions src/UniGetUI.PackageEngine.Tests/PackageTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ public void Dispose()
// onto the final release must remain visible rather than being read as 2.0.0.1 >= 2.0.0.
[InlineData("2.0.0_rc1", "2.0.0", false)]
[InlineData("2.0.0_beta2", "2.0.0", false)]
[InlineData("37149234619", "38756550835", false)]
[InlineData("38756550835", "37149234619", true)]
[InlineData("99999999999999999999", "100000000000000000000", false)]
[InlineData("1.0.0+20260101123456", "1.0.1", false)]
public async Task NewerVersionIsInstalled_ReturnsExpectedResult(string installedVersions, string newVersion, bool expected)
{
var manager = new PackageManagerBuilder().Build();
Expand Down
Loading