#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);
}
}
}
}