using System; using System.Text.RegularExpressions; namespace Katteker.AppStub { /// /// A hybrid implementation of SemVer that supports semantic versioning as described at http://semver.org while not /// strictly enforcing it to /// allow older 4-digit versioning schemes to continue working. /// [Serializable] //[TypeConverter(typeof(SemanticVersionTypeConverter))] public sealed class SemanticVersion : IComparable, IComparable, IEquatable { private const RegexOptions Flags = RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture; private static readonly Regex SemanticVersionRegex = new Regex(@"^(?\d+(\s*\.\s*\d+){0,3})(?-[a-z][0-9a-z-]*)?$", Flags); private static readonly Regex StrictSemanticVersionRegex = new Regex(@"^(?\d+(\.\d+){2})(?-[a-z][0-9a-z-]*)?$", Flags); private readonly string _originalString; public SemanticVersion(string version) : this(Parse(version)) { // The constructor normalizes the version string so that it we do not need to normalize it every time we need to operate on it. // The original string represents the original form in which the version is represented to be used when printing. _originalString = version; } public SemanticVersion(int major, int minor, int build, int revision) : this(new Version(major, minor, build, revision)) { } public SemanticVersion(int major, int minor, int build, string specialVersion) : this(new Version(major, minor, build), specialVersion) { } public SemanticVersion(Version version) : this(version, string.Empty) { } public SemanticVersion(Version version, string specialVersion) : this(version, specialVersion, null) { } private SemanticVersion(Version version, string specialVersion, string originalString) { if (version == null) throw new ArgumentNullException(nameof(version)); Version = NormalizeVersionValue(version); SpecialVersion = specialVersion ?? string.Empty; _originalString = string.IsNullOrEmpty(originalString) ? version + (!string.IsNullOrEmpty(specialVersion) ? '-' + specialVersion : null) : originalString; } internal SemanticVersion(SemanticVersion semVer) { _originalString = semVer.ToString(); Version = semVer.Version; SpecialVersion = semVer.SpecialVersion; } /// /// Gets the normalized version portion. /// public Version Version { get; } /// /// Gets the optional special version. /// public string SpecialVersion { get; } public int CompareTo(object obj) { if (ReferenceEquals(obj, null)) return 1; var other = obj as SemanticVersion; if (other == null) throw new ArgumentException("Type Must Be A Semantic Version", nameof(obj)); return CompareTo(other); } public int CompareTo(SemanticVersion other) { if (ReferenceEquals(other, null)) return 1; var result = Version.CompareTo(other.Version); if (result != 0) return result; var empty = string.IsNullOrEmpty(SpecialVersion); var otherEmpty = string.IsNullOrEmpty(other.SpecialVersion); if (empty && otherEmpty) return 0; if (empty) return 1; if (otherEmpty) return -1; return StringComparer.OrdinalIgnoreCase.Compare(SpecialVersion, other.SpecialVersion); } public bool Equals(SemanticVersion other) { return !ReferenceEquals(null, other) && Version.Equals(other.Version) && SpecialVersion.Equals(other.SpecialVersion, StringComparison.OrdinalIgnoreCase); } public string[] GetOriginalVersionComponents() { if (!string.IsNullOrEmpty(_originalString)) { // search the start of the SpecialVersion part, if any var dashIndex = _originalString.IndexOf('-'); var original = dashIndex != -1 ? _originalString.Substring(0, dashIndex) : _originalString; return SplitAndPadVersionString(original); } return SplitAndPadVersionString(Version.ToString()); } private static string[] SplitAndPadVersionString(string version) { var a = version.Split('.'); if (a.Length == 4) { return a; } // if 'a' has less than 4 elements, we pad the '0' at the end // to make it 4. var b = new string[4] {"0", "0", "0", "0"}; Array.Copy(a, 0, b, 0, a.Length); return b; } /// /// Parses a version string using loose semantic versioning rules that allows 2-4 version components followed by an /// optional special version. /// public static SemanticVersion Parse(string version) { if (string.IsNullOrEmpty(version)) throw new ArgumentException(nameof(version)); if (!TryParse(version, out var semVer)) throw new ArgumentException("Invalid Version String", nameof(version)); return semVer; } /// /// Parses a version string using loose semantic versioning rules that allows 2-4 version components followed by an /// optional special version. /// public static bool TryParse(string version, out SemanticVersion value) { return TryParseInternal(version, SemanticVersionRegex, out value); } /// /// Parses a version string using strict semantic versioning rules that allows exactly 3 components and an optional /// special version. /// public static bool TryParseStrict(string version, out SemanticVersion value) { return TryParseInternal(version, StrictSemanticVersionRegex, out value); } private static bool TryParseInternal(string version, Regex regex, out SemanticVersion semVer) { semVer = null; if (string.IsNullOrEmpty(version)) return false; var match = regex.Match(version.Trim()); if (!match.Success || !Version.TryParse(match.Groups["Version"].Value, out var versionValue)) return false; semVer = new SemanticVersion(NormalizeVersionValue(versionValue), match.Groups["Release"].Value.TrimStart('-'), version.Replace(" ", "")); return true; } /// /// Attempts to parse the version token as a SemanticVersion. /// /// An instance of SemanticVersion if it parses correctly, null otherwise. public static SemanticVersion ParseOptionalVersion(string version) { TryParse(version, out var semVer); return semVer; } private static Version NormalizeVersionValue(Version version) { return new Version(version.Major, version.Minor, Math.Max(version.Build, 0), Math.Max(version.Revision, 0)); } public static bool operator ==(SemanticVersion version1, SemanticVersion version2) { if (ReferenceEquals(version1, null)) return ReferenceEquals(version2, null); return version1.Equals(version2); } public static bool operator !=(SemanticVersion version1, SemanticVersion version2) { return !(version1 == version2); } public static bool operator <(SemanticVersion version1, SemanticVersion version2) { if (version1 == null) throw new ArgumentNullException(nameof(version1)); return version1.CompareTo(version2) < 0; } public static bool operator <=(SemanticVersion version1, SemanticVersion version2) { return version1 == version2 || version1 < version2; } public static bool operator >(SemanticVersion version1, SemanticVersion version2) { if (version1 == null) throw new ArgumentNullException(nameof(version1)); return version2 < version1; } public static bool operator >=(SemanticVersion version1, SemanticVersion version2) { return version1 == version2 || version1 > version2; } public override string ToString() { return _originalString; } public override bool Equals(object obj) { var semVer = obj as SemanticVersion; return !ReferenceEquals(null, semVer) && Equals(semVer); } public override int GetHashCode() { var hashCode = Version.GetHashCode(); if (SpecialVersion != null) hashCode = hashCode * 4567 + SpecialVersion.GetHashCode(); return hashCode; } } }