#pragma warning disable SA1512,SA1124 // Single-line comments should not be followed by blank line using System; using System.Globalization; using System.Text; using JetBrains.Annotations; using UnityEngine; #region No ReShaper // ReSharper disable All // needed because 'JetBrains.Annotations.NotNull' and 'System.Diagnostics.CodeAnalysis.NotNull' collide if this file is compiled with a never version of Unity / C# using SuppressMessageAttribute = System.Diagnostics.CodeAnalysis.SuppressMessageAttribute; // ReSharper restore All #endregion #pragma warning restore SA1512,SA1124 // Single-line comments should not be followed by blank line namespace NugetForUnity.Models { /// /// Represents a NuGet package version number. /// [Serializable] public sealed class NugetPackageVersion : IEquatable, IComparable, ISerializationCallbackReceiver { [NonSerialized] private SemVer2Version? maximumSemVer2Version; [NonSerialized] private SemVer2Version? minimumSemVer2Version; [NonSerialized] private SemVer2Version semVer2Version; /// /// Initializes a new instance of the class. /// public NugetPackageVersion() { SetFromString(null); } /// /// Initializes a new instance of the class with the given Version string. /// /// The version number as string. public NugetPackageVersion([CanBeNull] string version) { SetFromString(version); } /// /// Gets the normalized version number of the NuGet package. /// This is the normalized version number without build-metadata e.g. 1.0.0+b3a8 is normalized to 1.0.0. /// [NotNull] public string NormalizedVersion { get; private set; } = string.Empty; /// /// Gets the full version number of the NuGet package. /// This contains the build-metadata e.g. 1.0.0+b3a8. /// [field: SerializeField] [NotNull] public string FullVersion { get; private set; } = string.Empty; /// /// Gets a value indicating whether the minimum version number (only valid when HasVersionRange is true) is inclusive (true) or exclusive (false). /// public bool IsMinInclusive { get; private set; } /// /// Gets a value indicating whether the maximum version number (only valid when HasVersionRange is true) is inclusive (true) or exclusive (false). /// public bool IsMaxInclusive { get; private set; } /// /// Gets the minimum version number of the NuGet package. Only valid when HasVersionRange is true. /// [CanBeNull] public string MinimumVersion { get; private set; } /// /// Gets the maximum version number of the NuGet package. Only valid when HasVersionRange is true. /// [CanBeNull] public string MaximumVersion => maximumSemVer2Version?.ToString(); /// /// Gets a value indicating whether this is a pre-release package or an official release package. /// public bool IsPrerelease { get; private set; } /// /// Gets a value indicating whether the version number specified is a range of values. /// public bool HasVersionRange { get; private set; } /// /// Checks to see if the first is less than the second. /// /// The first to compare. /// The second to compare. /// True if the first is less than the second. public static bool operator <([CanBeNull] NugetPackageVersion first, [CanBeNull] NugetPackageVersion second) { if (first is null) { return true; } return first.CompareTo(second) < 0; } /// /// Checks to see if the first is greater than the second. /// /// The first to compare. /// The second to compare. /// True if the first is greater than the second. public static bool operator >([CanBeNull] NugetPackageVersion first, [CanBeNull] NugetPackageVersion second) { if (first is null) { return false; } return first.CompareTo(second) > 0; } /// /// Checks to see if the first is less than or equal to the second. /// /// The first to compare. /// The second to compare. /// True if the first is less than or equal to the second. public static bool operator <=([CanBeNull] NugetPackageVersion first, [CanBeNull] NugetPackageVersion second) { if (first is null) { return second is null; } return first.CompareTo(second) <= 0; } /// /// Checks to see if the first is greater than or equal to the second. /// /// The first to compare. /// The second to compare. /// True if the first is greater than or equal to the second. public static bool operator >=([CanBeNull] NugetPackageVersion first, [CanBeNull] NugetPackageVersion second) { if (first is null) { return second is null; } return first.CompareTo(second) >= 0; } /// /// Checks to see if the first is equal to the second. /// They are equal if the Id and the Version match. /// /// The first to compare. /// The second to compare. /// True if the first is equal to the second. public static bool operator ==([CanBeNull] NugetPackageVersion first, [CanBeNull] NugetPackageVersion second) { if (first is null) { return second is null; } return first.Equals(second); } /// /// Checks to see if the first is not equal to the second. /// They are not equal if the Id or the Version differ. /// /// The first to compare. /// The second to compare. /// True if the first is not equal to the second. public static bool operator !=(NugetPackageVersion first, NugetPackageVersion second) { return !(first == second); } /// /// Compares this version with the string representation of . /// /// The other version number to check if it is grater or equal to this version. /// /// -1 if other is less than this. /// 0 if other is equal to this. /// +1 if other is greater than this. /// public int CompareTo([CanBeNull] NugetPackageVersion other) { if (other is null) { return -1; } if (HasVersionRange || other.HasVersionRange) { return string.Compare(NormalizedVersion, other.NormalizedVersion, StringComparison.OrdinalIgnoreCase); } return semVer2Version.Compare(other.semVer2Version); } /// public void OnBeforeSerialize() { // nothing to do } /// public void OnAfterDeserialize() { SetFromString(FullVersion); } /// /// Determines if the given version is in the version range of this . /// See here: https://docs.nuget.org/ndocs/create-packages/dependency-versions. /// /// The to check if is in the range. /// True if the given version is in the range, otherwise false. public bool InRange([NotNull] NugetPackageVersion other) { if (string.IsNullOrEmpty(FullVersion)) { // if this has no version specified, it matches everything return true; } if (other.HasVersionRange) { if (!HasVersionRange) { // A 'single' version can't be in range of a version range. return false; } // We have two version ranges. We check if they intersect. return (other.minimumSemVer2Version.HasValue && CompareVersion(other.minimumSemVer2Version.Value) == 0 && (other.IsMinInclusive || !other.minimumSemVer2Version.Equals(maximumSemVer2Version))) || (other.maximumSemVer2Version.HasValue && CompareVersion(other.maximumSemVer2Version.Value) == 0 && (other.IsMaxInclusive || !other.maximumSemVer2Version.Equals(minimumSemVer2Version))); } if (!HasVersionRange) { // if it has no version range specified (e.g. only a single version number) // The NuGet's specs state that it is the minimum version number, inclusive return semVer2Version.Compare(other.semVer2Version) <= 0; } // version comparison with respect to the version range return CompareVersion(other.semVer2Version) == 0; } /// /// Determines if a given object is equal to this . /// /// The object to check. /// True if the given object is equal to this , otherwise false. public override bool Equals(object obj) { return Equals(obj as NugetPackageVersion); } /// /// Checks to see if this is equal to the given one. /// /// The other to check equality with. /// True if the package versions are equal, otherwise false. public bool Equals(NugetPackageVersion other) { if (other is null) { return false; } return string.Equals(NormalizedVersion, other.NormalizedVersion, StringComparison.OrdinalIgnoreCase); } /// /// Gets the hash-code for this . /// /// The hash-code for this instance. [SuppressMessage( "ReSharper", "NonReadonlyMemberInGetHashCode", Justification = "We only edit the version before we use the hash (stroe it in a dictionary).")] public override int GetHashCode() { #if UNITY_2021_2_OR_NEWER return NormalizedVersion.GetHashCode(StringComparison.OrdinalIgnoreCase); #else return StringComparer.OrdinalIgnoreCase.GetHashCode(NormalizedVersion); #endif } /// /// Returns the string representation of this . /// /// The version string. public override string ToString() { return FullVersion; } /// /// Compares the given version string with the version range of this . /// See here: https://docs.nuget.org/ndocs/create-packages/dependency-versions. /// /// The version to check if is in the range. /// /// -1 if other is less than the version range. 0 if other is inside the version range. +1 if other is greater than the /// version range. /// private int CompareVersion(in SemVer2Version otherSemVer2) { if (!HasVersionRange) { return semVer2Version.Compare(otherSemVer2); } var compareMinimum = 0; if (minimumSemVer2Version.HasValue) { compareMinimum = minimumSemVer2Version.Value.Compare(otherSemVer2); // -1 = Min < other <-- Inclusive & Exclusive // 0 = Min = other <-- Inclusive Only // +1 = Min > other <-- OUT OF RANGE if (IsMinInclusive) { if (compareMinimum > 0) { return -1; } } else { if (compareMinimum >= 0) { return -1; } } } if (maximumSemVer2Version.HasValue) { var compare = maximumSemVer2Version.Value.Compare(otherSemVer2); // -1 = Max < other <-- OUT OF RANGE // 0 = Max = other <-- Inclusive Only // +1 = Max > other <-- Inclusive & Exclusive if (IsMaxInclusive) { if (compare < 0) { return 1; } } else { if (compare <= 0) { return 1; } } } else { if (IsMaxInclusive) { // if there is no MaxVersion specified, but the Max is Inclusive, then it is an EXACT version match with the stored MINIMUM return compareMinimum; } } return 0; } private void SetFromString([CanBeNull] string version) { minimumSemVer2Version = null; maximumSemVer2Version = null; if (string.IsNullOrWhiteSpace(version)) { NormalizedVersion = string.Empty; FullVersion = string.Empty; semVer2Version = new SemVer2Version(false); return; } version = version.Trim(); FullVersion = version; IsMinInclusive = version.StartsWith("[", StringComparison.Ordinal); HasVersionRange = IsMinInclusive || version.StartsWith("(", StringComparison.Ordinal); if (HasVersionRange) { semVer2Version = new SemVer2Version(false); NormalizedVersion = version; IsPrerelease = version.IndexOf('-') >= 0; IsMaxInclusive = version.EndsWith("]", StringComparison.Ordinal); // if there is no MaxVersion specified, but the Max is Inclusive, then it is an EXACT version match with the stored MINIMUM var minMax = version.TrimStart('[', '(').TrimEnd(']', ')').Split(','); var minimumVersion = minMax[0].Trim(); minimumSemVer2Version = string.IsNullOrEmpty(minimumVersion) ? (SemVer2Version?)null : new SemVer2Version(minimumVersion); var maximumVersion = minMax.Length == 2 ? minMax[1].Trim() : null; maximumSemVer2Version = string.IsNullOrEmpty(maximumVersion) ? (SemVer2Version?)null : new SemVer2Version(maximumVersion); } else { semVer2Version = new SemVer2Version(version); NormalizedVersion = semVer2Version.ToString(); // normalize the version string IsPrerelease = semVer2Version.PreRelease != null; IsMaxInclusive = false; } } /// /// SemVer2 . /// Complying with NuGet comparison rules . /// /// Ignore spelling: SemVer, Sem, Ver private readonly struct SemVer2Version { [CanBeNull] private readonly string buildMetadata; private readonly int major; private readonly int minor; private readonly int patch; [CanBeNull] private readonly string[] preReleaseLabels; private readonly int revision; /// /// Initializes a new instance of the struct. /// /// Dummy to allow calling this constructor. public SemVer2Version(bool dummy) { _ = dummy; buildMetadata = null; PreRelease = null; major = -1; minor = -1; patch = -1; revision = -1; preReleaseLabels = null; } /// /// Initializes a new instance of the struct. /// /// The version number as string. public SemVer2Version([CanBeNull] string version) { if (!string.IsNullOrWhiteSpace(version)) { try { buildMetadata = null; var buildMetadataStartIndex = version.IndexOf('+'); if (buildMetadataStartIndex > 0) { buildMetadata = version.Substring(buildMetadataStartIndex + 1); version = version.Substring(0, buildMetadataStartIndex); } PreRelease = null; preReleaseLabels = null; var preReleaseStartIndex = version.IndexOf('-'); if (preReleaseStartIndex > 0) { PreRelease = version.Substring(preReleaseStartIndex + 1); preReleaseLabels = PreRelease.Split('.'); version = version.Substring(0, preReleaseStartIndex); } var split = version.Split('.'); major = int.Parse(split[0], CultureInfo.InvariantCulture); minor = 0; if (split.Length >= 2) { minor = int.Parse(split[1], CultureInfo.InvariantCulture); } patch = 0; if (split.Length >= 3) { patch = int.Parse(split[2], CultureInfo.InvariantCulture); } revision = 0; if (split.Length >= 4) { revision = int.Parse(split[3], CultureInfo.InvariantCulture); } return; } catch (Exception ex) { Debug.LogErrorFormat("Invalid version number: '{0}'\n{1}", version, ex); } } buildMetadata = null; PreRelease = null; preReleaseLabels = null; major = -1; minor = -1; patch = -1; revision = -1; } [CanBeNull] public string PreRelease { get; } /// /// Compares two version numbers in the form "1.2". Also supports an optional 3rd and 4th number as well as a prerelease tag, such as /// "1.3.0.1-alpha2". /// Returns: /// -1 if this is less than other /// 0 if this is equal to other /// +1 if this is greater than other. /// /// The second version number to compare. /// -1 if this is less than other. 0 if this is equal to other. +1 if this is greater than other. public int Compare(in SemVer2Version other) { try { var majorComparison = major.CompareTo(other.major); if (majorComparison == 0) { // if major versions are equal, compare minor versions var minorComparison = minor.CompareTo(other.minor); if (minorComparison == 0) { var patchNumber = patch; var otherPatch = other.patch; var patchComparison = patchNumber.CompareTo(otherPatch); if (patchComparison == 0) { // if patch versions are equal, compare revision of the versions var revisionNumber = revision; var otherRevision = other.revision; var revisionComparison = revisionNumber.CompareTo(otherRevision); if (revisionComparison == 0) { // if the build versions are equal, just return the prerelease version comparison if (preReleaseLabels == null && other.preReleaseLabels == null) { // no pre-release and the rest is equal return 0; } if (preReleaseLabels != null && other.preReleaseLabels == null) { // pre-release versions are always after release versions return -1; } if (preReleaseLabels == null && other.preReleaseLabels != null) { return 1; } var prereleaseComparison = ComparePreReleaseLabels(preReleaseLabels, other.preReleaseLabels); return prereleaseComparison; } // the build versions are different, so use them return revisionComparison; } // the patch versions are different, so use them return patchComparison; } // the minor versions are different, so use them return minorComparison; } // the major versions are different, so use them return majorComparison; } catch (Exception exception) { Debug.LogErrorFormat("Error: {0} while comparing '{1}' with '{2}'.", exception, this, other); return -1; } } /// public override string ToString() { return ToString(false); } /// /// Returns the version number as a string in the form ..-+ /// . /// The can be removed. /// /// If true the is included. /// The formatted string. [NotNull] public string ToString(bool withBuildMetadata) { var stringBuilder = new StringBuilder(); stringBuilder.Append(major); stringBuilder.Append('.'); stringBuilder.Append(minor); stringBuilder.Append('.'); stringBuilder.Append(patch); if (revision != 0) { stringBuilder.Append('.'); stringBuilder.Append(revision); } if (!string.IsNullOrEmpty(PreRelease)) { stringBuilder.Append('-'); stringBuilder.Append(PreRelease); } if (withBuildMetadata && !string.IsNullOrEmpty(buildMetadata)) { stringBuilder.Append('+'); stringBuilder.Append(buildMetadata); } return stringBuilder.ToString(); } /// /// Compares sets of Pre-Release labels ( splitted by '.'). /// private static int ComparePreReleaseLabels([NotNull] string[] releaseLabels1, [NotNull] string[] releaseLabels2) { var result = 0; var count = Math.Max(releaseLabels1.Length, releaseLabels2.Length); for (var i = 0; i < count; i++) { var hasLabel1 = i < releaseLabels1.Length; var hasLabel2 = i < releaseLabels2.Length; if (!hasLabel1 && hasLabel2) { return -1; } if (hasLabel1 && !hasLabel2) { return 1; } // compare the labels result = ComparePreReleaseLabel(releaseLabels1[i], releaseLabels2[i]); if (result != 0) { return result; } } return result; } /// /// Pre-Release labels are compared as numbers if they are numeric, otherwise they will be compared as strings (case insensitive). /// private static int ComparePreReleaseLabel(string releaseLabel1, string releaseLabel2) { var label1IsNumeric = int.TryParse(releaseLabel1, out var releaseLabel1Number); var label2IsNumeric = int.TryParse(releaseLabel2, out var releaseLabel2Number); if (label1IsNumeric && label2IsNumeric) { // if both are numeric compare them as numbers return releaseLabel1Number.CompareTo(releaseLabel2Number); } if (label1IsNumeric || label2IsNumeric) { // numeric labels come before alpha labels return label1IsNumeric ? -1 : 1; } // Everything will be compared case insensitively. return string.Compare(releaseLabel1, releaseLabel2, StringComparison.OrdinalIgnoreCase); } } } }