diff --git a/src/UniGetUI.Core.Tools.Tests/ToolsTests.cs b/src/UniGetUI.Core.Tools.Tests/ToolsTests.cs index 3af9b26706..d5dff4e6e8 100644 --- a/src/UniGetUI.Core.Tools.Tests/ToolsTests.cs +++ b/src/UniGetUI.Core.Tools.Tests/ToolsTests.cs @@ -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 diff --git a/src/UniGetUI.Core.Tools/Tools.cs b/src/UniGetUI.Core.Tools/Tools.cs index aeb1df162f..204559130c 100644 --- a/src/UniGetUI.Core.Tools/Tools.cs +++ b/src/UniGetUI.Core.Tools/Tools.cs @@ -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; @@ -534,10 +535,10 @@ public struct Version : IComparable, IComparable, IEquatable { 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; /// /// Segments past the fourth, with trailing zeroes trimmed so that equal versions @@ -546,12 +547,23 @@ public struct Version : IComparable, IComparable, IEquatable /// (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. /// - private readonly int[]? _extraSegments; + private readonly BigInteger[]? _extraSegments; - 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; @@ -564,16 +576,16 @@ private Version(int major, int minor, int patch, int remainder, int[]? extraSegm /// Builds a Version from an arbitrary number of segments. Segments past the fourth are /// retained for comparison rather than being folded into Remainder. /// - internal static Version FromSegments(IReadOnlyList segments) + internal static Version FromSegments(IReadOnlyList 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]; } @@ -586,12 +598,14 @@ internal static Version FromSegments(IReadOnlyList segments) extra ); - static int Segment(IReadOnlyList values, int index) => - index < values.Count ? values[index] : 0; + static BigInteger Segment(IReadOnlyList 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; @@ -669,7 +683,7 @@ public override int GetHashCode() 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(); } @@ -732,25 +746,34 @@ public static Version VersionStringToStruct(string version) // 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 segmentDigits = [""]; + List 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 @@ -760,6 +783,24 @@ public static Version VersionStringToStruct(string version) } } + 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 diff --git a/src/UniGetUI.PackageEngine.Tests/PackageTests.cs b/src/UniGetUI.PackageEngine.Tests/PackageTests.cs index c5fb2995ac..e1f1ea88e9 100644 --- a/src/UniGetUI.PackageEngine.Tests/PackageTests.cs +++ b/src/UniGetUI.PackageEngine.Tests/PackageTests.cs @@ -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();