Files
DHR60 4a59c7f4d6 Lock sing-box max version (#10091)
* Refactor Semantic Version

* Lock max version

* Allow sing-box update to pre-release
2026-09-04 10:17:31 +08:00

255 lines
6.7 KiB
C#

namespace ServiceLib.Models.Dto;
public class SemanticVersion : IEquatable<SemanticVersion>, IComparable
{
private readonly string? build;
private readonly int major;
private readonly int minor;
private readonly int patch;
private readonly string? prerelease;
private readonly string raw;
public SemanticVersion(int major, int minor, int patch)
{
this.major = major;
this.minor = minor;
this.patch = patch;
raw = $"{major}.{minor}.{patch}";
}
public SemanticVersion(string? version)
{
try
{
if (string.IsNullOrEmpty(version))
{
major = 0;
minor = 0;
patch = 0;
raw = $"{major}.{minor}.{patch}";
return;
}
raw = version;
var span = version.StartsWith("v", StringComparison.OrdinalIgnoreCase)
? version.AsSpan(1)
: version.AsSpan();
var plusIdx = span.IndexOf('+');
var metadataSpan = plusIdx >= 0 ? span[(plusIdx + 1)..] : [];
var leftOfPlus = plusIdx >= 0 ? span[..plusIdx] : span;
var dashIdx = leftOfPlus.IndexOf('-');
var versionSpan = dashIdx >= 0 ? leftOfPlus[..dashIdx] : leftOfPlus;
var preReleaseSpan = dashIdx >= 0 ? leftOfPlus[(dashIdx + 1)..] : [];
build = metadataSpan.Length > 0 ? metadataSpan.ToString() : null;
prerelease = preReleaseSpan.Length > 0 ? preReleaseSpan.ToString() : null;
var parts = versionSpan.ToString().Split('.');
switch (parts.Length)
{
case 2:
major = int.Parse(parts.First());
minor = int.Parse(parts.Last());
patch = 0;
break;
case 3 or 4:
major = int.Parse(parts[0]);
minor = int.Parse(parts[1]);
patch = int.Parse(parts[2]);
break;
default:
throw new ArgumentException("Invalid version string");
}
}
catch
{
major = 0;
minor = 0;
patch = 0;
raw = $"{major}.{minor}.{patch}";
}
}
public bool Equals(SemanticVersion? other)
{
if (other is null)
{
return false;
}
if (ReferenceEquals(this, other))
{
return true;
}
return major == other.major && minor == other.minor && patch == other.patch && prerelease == other.prerelease;
}
public static bool TryParse(string? version, out SemanticVersion? result)
{
try
{
result = new SemanticVersion(version);
return true;
}
catch
{
result = null;
return false;
}
}
public override bool Equals(object? obj)
{
return obj is SemanticVersion other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(major, minor, patch, prerelease);
}
public override string ToString()
{
return raw;
}
public string ToStandardVersionString(string? prefix = null)
{
var sb = new StringBuilder();
if (!string.IsNullOrEmpty(prefix))
{
sb.Append(prefix);
}
sb.Append($"{major}.{minor}.{patch}");
if (!string.IsNullOrEmpty(prerelease))
{
sb.Append($"-{prerelease}");
}
if (!string.IsNullOrEmpty(build))
{
sb.Append($"+{build}");
}
return sb.ToString();
}
public static bool operator <(SemanticVersion left, SemanticVersion right)
{
return left.CompareTo(right) < 0;
}
public static bool operator >(SemanticVersion left, SemanticVersion right)
{
return left.CompareTo(right) > 0;
}
public static bool operator <=(SemanticVersion left, SemanticVersion right)
{
return left.CompareTo(right) <= 0;
}
public static bool operator >=(SemanticVersion left, SemanticVersion right)
{
return left.CompareTo(right) >= 0;
}
#region Private
public int CompareTo(SemanticVersion other)
{
if (major != other.major)
{
return major.CompareTo(other.major);
}
if (minor != other.minor)
{
return minor.CompareTo(other.minor);
}
if (patch != other.patch)
{
return patch.CompareTo(other.patch);
}
return ComparePreRelease(prerelease, other.prerelease);
}
public int CompareTo(object? obj)
{
if (obj is null)
{
return 1;
}
if (obj is SemanticVersion other)
{
return CompareTo(other);
}
throw new ArgumentException("Object is not a SemanticVersion");
}
private static int ComparePreRelease(string? left, string? right)
{
if (string.IsNullOrEmpty(left) && string.IsNullOrEmpty(right))
{
return 0;
}
if (string.IsNullOrEmpty(left))
{
return 1;
}
if (string.IsNullOrEmpty(right))
{
return -1;
}
var leftSpan = left.AsSpan();
var rightSpan = right.AsSpan();
using var leftEnum = leftSpan.Split('.').GetEnumerator();
using var rightEnum = rightSpan.Split('.').GetEnumerator();
while (true)
{
var hasLeft = leftEnum.MoveNext();
var hasRight = rightEnum.MoveNext();
if (!hasLeft && !hasRight)
{
return 0;
}
if (!hasLeft)
{
return -1;
}
if (!hasRight)
{
return 1;
}
var leftSegment = leftSpan[leftEnum.Current];
var rightSegment = rightSpan[rightEnum.Current];
var segCmp = CompareSegment(leftSegment, rightSegment);
if (segCmp != 0)
{
return segCmp;
}
}
}
private static int CompareSegment(ReadOnlySpan<char> left, ReadOnlySpan<char> right)
{
var leftIsNum = ulong.TryParse(left, out var leftNum);
var rightIsNum = ulong.TryParse(right, out var rightNum);
return (leftIsNum, rightIsNum) switch
{
(true, true) => leftNum.CompareTo(rightNum),
(false, true) => 1,
(true, false) => -1,
(false, false) => left.SequenceCompareTo(right),
};
}
#endregion Private
}