diff --git a/v2rayN/ServiceLib/Manager/CoreInfoManager.cs b/v2rayN/ServiceLib/Manager/CoreInfoManager.cs index bda3108a..de5875e5 100644 --- a/v2rayN/ServiceLib/Manager/CoreInfoManager.cs +++ b/v2rayN/ServiceLib/Manager/CoreInfoManager.cs @@ -229,6 +229,7 @@ public sealed class CoreInfoManager DownloadUrlLinuxLoong64 = urlSingbox + "/download/{0}/sing-box-{1}-linux-loong64.tar.gz", DownloadUrlOSX64 = urlSingbox + "/download/{0}/sing-box-{1}-darwin-amd64.tar.gz", DownloadUrlOSXArm64 = urlSingbox + "/download/{0}/sing-box-{1}-darwin-arm64.tar.gz", + LockedMaxVersion = new SemanticVersion(1, 14, int.MaxValue), Match = "sing-box", VersionArg = "version", }, diff --git a/v2rayN/ServiceLib/Models/CoreConfigs/CoreInfo.cs b/v2rayN/ServiceLib/Models/CoreConfigs/CoreInfo.cs index 1cb23af0..df5f3667 100644 --- a/v2rayN/ServiceLib/Models/CoreConfigs/CoreInfo.cs +++ b/v2rayN/ServiceLib/Models/CoreConfigs/CoreInfo.cs @@ -16,6 +16,7 @@ public class CoreInfo public string? DownloadUrlLinuxLoong64 { get; set; } public string? DownloadUrlOSX64 { get; set; } public string? DownloadUrlOSXArm64 { get; set; } + public SemanticVersion? LockedMaxVersion { get; set; } public string? Match { get; set; } public string? VersionArg { get; set; } public bool AbsolutePath { get; set; } diff --git a/v2rayN/ServiceLib/Models/Dto/SemanticVersion.cs b/v2rayN/ServiceLib/Models/Dto/SemanticVersion.cs index 1e66353c..8af2f5fb 100644 --- a/v2rayN/ServiceLib/Models/Dto/SemanticVersion.cs +++ b/v2rayN/ServiceLib/Models/Dto/SemanticVersion.cs @@ -1,18 +1,20 @@ namespace ServiceLib.Models.Dto; -public class SemanticVersion +public class SemanticVersion : IEquatable, IComparable { + private readonly string? build; private readonly int major; private readonly int minor; private readonly int patch; - private readonly string version; + 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; - version = $"{major}.{minor}.{patch}"; + raw = $"{major}.{minor}.{patch}"; } public SemanticVersion(string? version) @@ -24,26 +26,41 @@ public class SemanticVersion major = 0; minor = 0; patch = 0; + raw = $"{major}.{minor}.{patch}"; return; } - this.version = version.RemovePrefix('v'); + raw = version; - var parts = this.version.Split('.'); - if (parts.Length == 2) + 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) { - major = int.Parse(parts.First()); - minor = int.Parse(parts.Last()); - patch = 0; - } - else if (parts.Length is 3 or 4) - { - major = int.Parse(parts[0]); - minor = int.Parse(parts[1]); - patch = int.Parse(parts[2]); - } - else - { - throw new ArgumentException("Invalid version string"); + 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 @@ -51,135 +68,186 @@ public class SemanticVersion 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) { - if (obj is SemanticVersion other) - { - return major == other.major && minor == other.minor && patch == other.patch; - } - else - { - return false; - } + return obj is SemanticVersion other && Equals(other); } public override int GetHashCode() { - return major.GetHashCode() ^ minor.GetHashCode() ^ patch.GetHashCode(); + return HashCode.Combine(major, minor, patch, prerelease); } - /// - /// Use ToVersionString(string? prefix) instead if possible. - /// - /// major.minor.patch public override string ToString() { - return version; + return raw; } - public string ToVersionString(string? prefix = null) + public string ToStandardVersionString(string? prefix = null) { - if (prefix == null) + var sb = new StringBuilder(); + if (!string.IsNullOrEmpty(prefix)) { - return version; + sb.Append(prefix); } - else + sb.Append($"{major}.{minor}.{patch}"); + if (!string.IsNullOrEmpty(prerelease)) { - return $"{prefix}{version}"; + sb.Append($"-{prerelease}"); } + if (!string.IsNullOrEmpty(build)) + { + sb.Append($"+{build}"); + } + return sb.ToString(); } - public static bool operator ==(SemanticVersion v1, SemanticVersion v2) - { return v1.Equals(v2); } + public static bool operator <(SemanticVersion left, SemanticVersion right) + { + return left.CompareTo(right) < 0; + } - public static bool operator !=(SemanticVersion v1, SemanticVersion v2) - { return !v1.Equals(v2); } + public static bool operator >(SemanticVersion left, SemanticVersion right) + { + return left.CompareTo(right) > 0; + } - public static bool operator >=(SemanticVersion v1, SemanticVersion v2) - { return v1.GreaterEquals(v2); } + public static bool operator <=(SemanticVersion left, SemanticVersion right) + { + return left.CompareTo(right) <= 0; + } - public static bool operator <=(SemanticVersion v1, SemanticVersion v2) - { return v1.LessEquals(v2); } + public static bool operator >=(SemanticVersion left, SemanticVersion right) + { + return left.CompareTo(right) >= 0; + } #region Private - private bool GreaterEquals(SemanticVersion other) + public int CompareTo(SemanticVersion other) { - if (major < other.major) + if (major != other.major) { - return false; + return major.CompareTo(other.major); } - else if (major > other.major) + if (minor != other.minor) { - return true; + return minor.CompareTo(other.minor); } - else + if (patch != other.patch) { - if (minor < other.minor) + 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 false; + return 0; } - else if (minor > other.minor) + if (!hasLeft) { - return true; + return -1; } - else + if (!hasRight) { - if (patch < other.patch) - { - return false; - } - else if (patch > other.patch) - { - return true; - } - else - { - return true; - } + return 1; + } + + var leftSegment = leftSpan[leftEnum.Current]; + var rightSegment = rightSpan[rightEnum.Current]; + + var segCmp = CompareSegment(leftSegment, rightSegment); + if (segCmp != 0) + { + return segCmp; } } } - private bool LessEquals(SemanticVersion other) + private static int CompareSegment(ReadOnlySpan left, ReadOnlySpan right) { - if (major < other.major) + var leftIsNum = ulong.TryParse(left, out var leftNum); + var rightIsNum = ulong.TryParse(right, out var rightNum); + + return (leftIsNum, rightIsNum) switch { - return true; - } - else if (major > other.major) - { - return false; - } - else - { - if (minor < other.minor) - { - return true; - } - else if (minor > other.minor) - { - return false; - } - else - { - if (patch < other.patch) - { - return true; - } - else if (patch > other.patch) - { - return false; - } - else - { - return true; - } - } - } + (true, true) => leftNum.CompareTo(rightNum), + + (false, true) => 1, + (true, false) => -1, + + (false, false) => left.SequenceCompareTo(right), + }; } #endregion Private diff --git a/v2rayN/ServiceLib/Services/UpdateService.cs b/v2rayN/ServiceLib/Services/UpdateService.cs index c38bed7d..11b1d3ef 100644 --- a/v2rayN/ServiceLib/Services/UpdateService.cs +++ b/v2rayN/ServiceLib/Services/UpdateService.cs @@ -1,6 +1,6 @@ namespace ServiceLib.Services; -public class UpdateService(Config config, Func updateFunc) +public partial class UpdateService(Config config, Func updateFunc) { private readonly Config? _config = config; private readonly Func? _updateFunc = updateFunc; @@ -174,7 +174,7 @@ public class UpdateService(Config config, Func updateFunc) { var coreInfo = CoreInfoManager.Instance.GetCoreInfo(type); var tagName = string.Empty; - if (preRelease) + if (preRelease || coreInfo?.LockedMaxVersion != null) { var url = coreInfo?.ReleaseApiUrl; var result = await downloadHandle.TryDownloadString(url, true, Global.AppName); @@ -187,6 +187,24 @@ public class UpdateService(Config config, Func updateFunc) var gitHubRelease = preRelease ? gitHubReleases?.First() : gitHubReleases?.First(r => r.Prerelease == false); tagName = gitHubRelease?.TagName; //var body = gitHubRelease?.Body; + + if (coreInfo?.LockedMaxVersion != null) + { + var lockedMaxVersion = coreInfo.LockedMaxVersion; + var remoteVersion = new SemanticVersion(tagName); + if (remoteVersion > lockedMaxVersion) + { + var fallbackRelease = gitHubReleases? + .Where(r => preRelease || !r.Prerelease) + .Select(r => new { Release = r, IsValid = SemanticVersion.TryParse(r.TagName, out var v), Version = v }) + .Where(x => x.IsValid && x.Version <= coreInfo.LockedMaxVersion) + .MaxBy(x => x.Version)? + .Release; + + gitHubRelease = fallbackRelease; + tagName = gitHubRelease?.TagName; + } + } } else { @@ -202,6 +220,9 @@ public class UpdateService(Config config, Func updateFunc) return new UpdateResult(true, new SemanticVersion(tagName)); } + [GeneratedRegex(@"v?(?\d+\.\d+\.\d+(?:-[0-9a-zA-Z.-]+)?(?:\+[0-9a-zA-Z.-]+)?)", RegexOptions.IgnoreCase)] + private static partial Regex SemVerRegex(); + private async Task GetCoreVersion(ECoreType type) { try @@ -227,23 +248,7 @@ public class UpdateService(Config config, Func updateFunc) var result = await Utils.GetCliWrapOutput(filePath, coreInfo.VersionArg); var echo = result ?? ""; - var version = string.Empty; - switch (type) - { - case ECoreType.v2fly: - case ECoreType.Xray: - case ECoreType.v2fly_v5: - version = Regex.Match(echo, $"{coreInfo.Match} ([0-9.]+) \\(").Groups[1].Value; - break; - - case ECoreType.mihomo: - version = Regex.Match(echo, $"v[0-9.]+").Groups[0].Value; - break; - - case ECoreType.sing_box: - version = Regex.Match(echo, $"([0-9.]+)").Groups[1].Value; - break; - } + var version = SemVerRegex().Match(echo).Groups["version"].Value; return new SemanticVersion(version); } catch (Exception ex) @@ -269,30 +274,25 @@ public class UpdateService(Config config, Func updateFunc) case ECoreType.v2fly: case ECoreType.Xray: case ECoreType.v2fly_v5: - { - curVersion = await GetCoreVersion(type); - message = string.Format(ResUI.IsLatestCore, type, curVersion.ToVersionString("v")); - url = string.Format(coreUrl, version.ToVersionString("v")); - break; - } case ECoreType.mihomo: { curVersion = await GetCoreVersion(type); - message = string.Format(ResUI.IsLatestCore, type, curVersion); - url = string.Format(coreUrl, version.ToVersionString("v")); + message = string.Format(ResUI.IsLatestCore, type, curVersion.ToStandardVersionString("v")); + url = string.Format(coreUrl, version); break; } + case ECoreType.sing_box: { curVersion = await GetCoreVersion(type); - message = string.Format(ResUI.IsLatestCore, type, curVersion.ToVersionString("v")); - url = string.Format(coreUrl, version.ToVersionString("v"), version); + message = string.Format(ResUI.IsLatestCore, type, curVersion.ToStandardVersionString("v")); + url = string.Format(coreUrl, version, version.ToString().RemovePrefix("v")); break; } case ECoreType.v2rayN: { curVersion = new SemanticVersion(Utils.GetVersionInfo()); - message = string.Format(ResUI.IsLatestN, type, curVersion); + message = string.Format(ResUI.IsLatestN, type, curVersion.ToStandardVersionString("v")); url = string.Format(coreUrl, version); break; } @@ -300,7 +300,7 @@ public class UpdateService(Config config, Func updateFunc) throw new ArgumentException("Type"); } - if (curVersion >= version && version != new SemanticVersion(0, 0, 0)) + if (curVersion >= version && !version.Equals(new SemanticVersion(0, 0, 0))) { return new UpdateResult(false, message); } diff --git a/v2rayN/ServiceLib/ViewModels/CheckUpdateViewModel.cs b/v2rayN/ServiceLib/ViewModels/CheckUpdateViewModel.cs index 9a2133f4..f9b8eb19 100644 --- a/v2rayN/ServiceLib/ViewModels/CheckUpdateViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/CheckUpdateViewModel.cs @@ -193,7 +193,7 @@ public partial class CheckUpdateViewModel : MyReactiveObject } await CheckUpdateN(EnableCheckPreReleaseUpdate); } - else if (item.CoreType == ECoreType.Xray) + else if (item.CoreType is ECoreType.Xray or ECoreType.sing_box) { await CheckUpdateCore(item, EnableCheckPreReleaseUpdate); }