// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Text; namespace NuGet { /// /// 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. /// internal sealed class SemanticVersion : IComparable, IComparable, IEquatable { private string _normalizedVersionString; public SemanticVersion(string version) : this(Parse(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) { if (version == null) { throw new ArgumentNullException(nameof(version)); } Version = NormalizeVersionValue(version); SpecialVersion = specialVersion ?? string.Empty; } internal SemanticVersion(SemanticVersion semVer) { Version = semVer.Version; SpecialVersion = semVer.SpecialVersion; } /// /// Gets the normalized version portion. /// public Version Version { get; private set; } /// /// Gets the optional special version. /// public string SpecialVersion { get; private set; } private static string[] SplitAndPadVersionString(string version) { string[] a = version.Split('.'); if (a.Length == 4) { return a; } else { // 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 ArgumentNullException(nameof(version)); } SemanticVersion semVer; if (!TryParse(version, out semVer)) { throw new ArgumentException(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, strict: false, semVer: 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, strict: true, semVer: out value); } private static bool TryParseInternal(string version, bool strict, out SemanticVersion semVer) { semVer = null; if (string.IsNullOrEmpty(version)) { return false; } version = version.Trim(); var versionPart = version; string specialVersion = string.Empty; if (version.IndexOf('-') != -1) { var parts = version.Split(new char[] { '-' }, 2, StringSplitOptions.RemoveEmptyEntries); if (parts.Length != 2) { return false; } versionPart = parts[0]; specialVersion = parts[1]; } Version versionValue; if (!Version.TryParse(versionPart, out versionValue)) { return false; } if (strict) { // Must have major, minor and build only. if (versionValue.Major == -1 || versionValue.Minor == -1 || versionValue.Build == -1 || versionValue.Revision != -1) { return false; } } semVer = new SemanticVersion(NormalizeVersionValue(versionValue), specialVersion); 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) { SemanticVersion semVer; TryParse(version, out 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 int CompareTo(object obj) { if (Object.ReferenceEquals(obj, null)) { return 1; } SemanticVersion other = obj as SemanticVersion; if (other == null) { throw new ArgumentException(nameof(obj)); } return CompareTo(other); } public int CompareTo(SemanticVersion other) { if (Object.ReferenceEquals(other, null)) { return 1; } int result = Version.CompareTo(other.Version); if (result != 0) { return result; } bool empty = string.IsNullOrEmpty(SpecialVersion); bool otherEmpty = string.IsNullOrEmpty(other.SpecialVersion); if (empty && otherEmpty) { return 0; } else if (empty) { return 1; } else if (otherEmpty) { return -1; } return StringComparer.OrdinalIgnoreCase.Compare(SpecialVersion, other.SpecialVersion); } public static bool operator ==(SemanticVersion version1, SemanticVersion version2) { if (Object.ReferenceEquals(version1, null)) { return Object.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() { if (_normalizedVersionString == null) { var builder = new StringBuilder(); builder .Append(Version.Major) .Append('.') .Append(Version.Minor) .Append('.') .Append(Math.Max(0, Version.Build)); if (Version.Revision > 0) { builder .Append('.') .Append(Version.Revision); } if (!string.IsNullOrEmpty(SpecialVersion)) { builder .Append('-') .Append(SpecialVersion); } _normalizedVersionString = builder.ToString(); } return _normalizedVersionString; } public bool Equals(SemanticVersion other) { return !Object.ReferenceEquals(null, other) && Version.Equals(other.Version) && SpecialVersion.Equals(other.SpecialVersion, StringComparison.OrdinalIgnoreCase); } public override bool Equals(object obj) { SemanticVersion semVer = obj as SemanticVersion; return !Object.ReferenceEquals(null, semVer) && Equals(semVer); } public override int GetHashCode() { int hashCode = Version.GetHashCode(); if (SpecialVersion != null) { hashCode = hashCode * 4567 + SpecialVersion.GetHashCode(); } return hashCode; } } }