// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Text.RegularExpressions; namespace EpicGames.Core { /// /// Wrapper for a semver version string (https://semver.org/) /// public struct SemVer : IEquatable, IComparable { ref struct Field { public readonly int Pos; public readonly ReadOnlySpan Text; public Field(int pos, ReadOnlySpan text) { Pos = pos; Text = text; } public int End => Pos + Text.Length; } readonly string _text; readonly int _length; ReadOnlySpan Span => _text.AsSpan(0, _length); // Regex taken from official site. static readonly Regex s_pattern = new Regex(@"^((0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?)(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"); /// /// Constructor /// private SemVer(string text, int length) { _text = text; _length = length; } /// /// Parses the given string as a semantic version /// /// Text to parse /// Resulting semver object public static SemVer Parse(string text) { SemVer result; if (!TryParse(text, out result)) { throw new ArgumentException($"String '{text}' is not a valid semantic version string. See https://semver.org/.", nameof(text)); } return result; } /// /// Attempt to parse the given string as a semantic version /// /// Text to parse /// Resulting version /// True on success public static bool TryParse(string text, out SemVer semVer) { Match match = s_pattern.Match(text); if (match.Success) { semVer = new SemVer(text, match.Groups[1].Length); return true; } else { semVer = default; return false; } } /// public override int GetHashCode() => String.GetHashCode(_text.AsSpan(0, _length), StringComparison.Ordinal); /// public override bool Equals(object? obj) => obj is SemVer other && Equals(other); /// public bool Equals(SemVer other) => Span.SequenceEqual(other.Span); /// public int CompareTo(SemVer other) => Compare(Span, other.Span); /// public override string ToString() => _text; /// /// Compare two versions /// /// First version to compare /// Second version to compare /// A number indicating the order of the two version strings public static int Compare(SemVer lhs, SemVer rhs) => Compare(lhs.Span, rhs.Span); static int Compare(ReadOnlySpan lhs, ReadOnlySpan rhs) { // Compare major fields Field lhsField = GetField(lhs, 0); Field rhsField = GetField(rhs, 0); int diff = CompareNumericFields(lhsField.Text, rhsField.Text); if (diff != 0) { return diff; } // Compare minor fields lhsField = GetField(lhs, lhsField.End + 1); rhsField = GetField(rhs, rhsField.End + 1); diff = CompareNumericFields(lhsField.Text, rhsField.Text); if (diff != 0) { return diff; } // Compare patch fields lhsField = GetField(lhs, lhsField.End + 1); rhsField = GetField(rhs, rhsField.End + 1); diff = CompareNumericFields(lhsField.Text, rhsField.Text); if (diff != 0) { return diff; } // If either is complete, it takes precedence if (lhsField.End == lhs.Length || rhsField.End == rhs.Length) { return ((lhsField.End == lhs.Length) ? 1 : 0) - ((rhsField.End == rhs.Length) ? 1 : 0); } // Otherwise compare each pre-release field while (lhsField.End < lhs.Length && rhsField.End < rhs.Length) { lhsField = GetField(lhs, lhsField.End + 1); rhsField = GetField(rhs, rhsField.End + 1); diff = CompareFields(lhsField.Text, rhsField.Text); if (diff != 0) { return diff; } } // Give precedence to the longer version string return ((lhsField.End == lhs.Length) ? 0 : 1) - ((rhsField.End == rhs.Length) ? 0 : 1); } static Field GetField(ReadOnlySpan text, int pos) { int end = pos + 1; while (end < text.Length && text[end] != '.' && text[end] != '-' && text[end] != '+') { end++; } return new Field(pos, text.Slice(pos, end - pos)); } static int CompareFields(ReadOnlySpan lhs, ReadOnlySpan rhs) { // Rules: // Identifiers consisting of only digits are compared numerically. // Identifiers with letters or hyphens are compared lexically in ASCII sort order. // Numeric identifiers always have lower precedence than non-numeric identifiers. if (IsNumeric(lhs)) { return IsNumeric(rhs) ? CompareNumericFields(lhs, rhs) : -1; } else { return IsNumeric(rhs) ? +1 : lhs.SequenceCompareTo(rhs); } } static int CompareNumericFields(ReadOnlySpan lhs, ReadOnlySpan rhs) { return (lhs.Length == rhs.Length) ? lhs.SequenceCompareTo(rhs) : (lhs.Length - rhs.Length); } static bool IsNumeric(ReadOnlySpan field) { for (int i = 0; i < field.Length; i++) { if (field[i] < '0' || field[i] > '9') { return false; } } return true; } /// /// Compares two versions for equality /// public static bool operator ==(SemVer lhs, SemVer rhs) => lhs.Equals(rhs); /// /// Compares two versions for inequality /// public static bool operator !=(SemVer lhs, SemVer rhs) => !lhs.Equals(rhs); /// /// Tests if one version has lower precedence than another /// public static bool operator <(SemVer lhs, SemVer rhs) => Compare(lhs.Span, rhs.Span) < 0; /// /// Tests if one version has lower or equal precedence to another /// public static bool operator <=(SemVer lhs, SemVer rhs) => Compare(lhs.Span, rhs.Span) <= 0; /// /// Tests if one version has greater precedence than another /// public static bool operator >(SemVer lhs, SemVer rhs) => Compare(lhs.Span, rhs.Span) > 0; /// /// Tests if one version has greater or equal precedence to another /// public static bool operator >=(SemVer lhs, SemVer rhs) => Compare(lhs.Span, rhs.Span) >= 0; } }