using System; using System.Globalization; using System.Linq; namespace LanMountainDesktop.Shared.Contracts.Deployment; /// /// 轻量 SemVer 2.0 解析与比较(支持 0.8.5-beta.1 等预发布版本)。 /// 安装器与启动器统一使用本类型比较版本,禁止使用 System.Version 解析渠道版本号。 /// public sealed class SemanticVersion : IComparable, IEquatable { public int Major { get; } public int Minor { get; } public int Patch { get; } public int Revision { get; } public string? Prerelease { get; } private SemanticVersion(int major, int minor, int patch, int revision, string? prerelease) { Major = major; Minor = minor; Patch = patch; Revision = revision; Prerelease = string.IsNullOrWhiteSpace(prerelease) ? null : prerelease; } public static bool TryParse(string? value, out SemanticVersion? parsed) { parsed = null; if (string.IsNullOrWhiteSpace(value)) { return false; } var normalized = value.Trim().TrimStart('v', 'V'); // 去掉 build metadata(+xxx) var plusIndex = normalized.IndexOf('+', StringComparison.Ordinal); if (plusIndex >= 0) { normalized = normalized[..plusIndex]; } string? prerelease = null; var dashIndex = normalized.IndexOf('-', StringComparison.Ordinal); if (dashIndex >= 0) { prerelease = normalized[(dashIndex + 1)..]; normalized = normalized[..dashIndex]; } var parts = normalized.Split('.', StringSplitOptions.RemoveEmptyEntries); if (parts.Length is < 2 or > 4) { return false; } var numbers = new int[4]; for (var i = 0; i < parts.Length; i++) { if (!int.TryParse(parts[i], NumberStyles.None, CultureInfo.InvariantCulture, out numbers[i]) || numbers[i] < 0) { return false; } } parsed = new SemanticVersion(numbers[0], numbers[1], numbers[2], numbers[3], prerelease); return true; } public static SemanticVersion Parse(string value) { return TryParse(value, out var parsed) ? parsed! : throw new FormatException($"Invalid semantic version: {value}"); } public int CompareTo(SemanticVersion? other) { if (other is null) { return 1; } var core = Major.CompareTo(other.Major); if (core != 0) return core; core = Minor.CompareTo(other.Minor); if (core != 0) return core; core = Patch.CompareTo(other.Patch); if (core != 0) return core; core = Revision.CompareTo(other.Revision); if (core != 0) return core; // SemVer: 无预发布 > 有预发布 if (Prerelease is null && other.Prerelease is null) return 0; if (Prerelease is null) return 1; if (other.Prerelease is null) return -1; return ComparePrerelease(Prerelease, other.Prerelease); } private static int ComparePrerelease(string left, string right) { var leftParts = left.Split('.'); var rightParts = right.Split('.'); var length = Math.Max(leftParts.Length, rightParts.Length); for (var i = 0; i < length; i++) { if (i >= leftParts.Length) return -1; if (i >= rightParts.Length) return 1; var l = leftParts[i]; var r = rightParts[i]; var lNumeric = l.All(char.IsAsciiDigit); var rNumeric = r.All(char.IsAsciiDigit); int result; if (lNumeric && rNumeric) { result = long.Parse(l, CultureInfo.InvariantCulture) .CompareTo(long.Parse(r, CultureInfo.InvariantCulture)); } else if (lNumeric) { result = -1; } else if (rNumeric) { result = 1; } else { result = string.CompareOrdinal(l, r); } if (result != 0) { return result; } } return 0; } public bool Equals(SemanticVersion? other) => other is not null && CompareTo(other) == 0; public override bool Equals(object? obj) => obj is SemanticVersion other && Equals(other); public override int GetHashCode() => HashCode.Combine(Major, Minor, Patch, Revision, Prerelease); public override string ToString() { var core = Revision > 0 ? $"{Major}.{Minor}.{Patch}.{Revision}" : $"{Major}.{Minor}.{Patch}"; return Prerelease is null ? core : $"{core}-{Prerelease}"; } public static bool operator >(SemanticVersion left, SemanticVersion right) => left.CompareTo(right) > 0; public static bool operator <(SemanticVersion left, SemanticVersion right) => left.CompareTo(right) < 0; public static bool operator >=(SemanticVersion left, SemanticVersion right) => left.CompareTo(right) >= 0; public static bool operator <=(SemanticVersion left, SemanticVersion right) => left.CompareTo(right) <= 0; }