VRCMelonAssistant/VRCMelonAssistant/Libs/semver/SemVersion.cs

599 lines
25 KiB
C#

/*
Copyright (c) 2013 Max Hauser
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
using System;
using System.Globalization;
using System.Text;
#if !NETSTANDARD
using System.Runtime.Serialization;
using System.Security.Permissions;
#endif
using System.Text.RegularExpressions;
namespace VRCMelonAssistant.Libs
{
/// <summary>
/// A semantic version implementation.
/// Conforms with v2.0.0 of http://semver.org
/// </summary>
#if NETSTANDARD
public sealed class SemVersion : IComparable<SemVersion>, IComparable
#else
[Serializable]
public sealed class SemVersion : IComparable<SemVersion>, IComparable, ISerializable
#endif
{
private static readonly Regex ParseEx =
new Regex(@"^(?<major>\d+)" +
@"(?>\.(?<minor>\d+))?" +
@"(?>\.(?<patch>\d+))?" +
@"(?>\-(?<pre>[0-9A-Za-z\-\.]+))?" +
@"(?>\+(?<build>[0-9A-Za-z\-\.]+))?$",
#if NETSTANDARD
RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture,
#else
RegexOptions.CultureInvariant | RegexOptions.Compiled | RegexOptions.ExplicitCapture,
#endif
TimeSpan.FromSeconds(0.5));
#if !NETSTANDARD
#pragma warning disable CA1801 // Parameter unused
/// <summary>
/// Deserialize a <see cref="SemVersion"/>.
/// </summary>
/// <exception cref="ArgumentNullException">The <paramref name="info"/> parameter is null.</exception>
private SemVersion(SerializationInfo info, StreamingContext context)
#pragma warning restore CA1801 // Parameter unused
{
if (info == null) throw new ArgumentNullException(nameof(info));
var semVersion = Parse(info.GetString("SemVersion"));
Major = semVersion.Major;
Minor = semVersion.Minor;
Patch = semVersion.Patch;
Prerelease = semVersion.Prerelease;
Build = semVersion.Build;
}
#endif
/// <summary>
/// Constructs a new instance of the <see cref="SemVersion" /> class.
/// </summary>
/// <param name="major">The major version.</param>
/// <param name="minor">The minor version.</param>
/// <param name="patch">The patch version.</param>
/// <param name="prerelease">The prerelease version (e.g. "alpha").</param>
/// <param name="build">The build metadata (e.g. "nightly.232").</param>
public SemVersion(int major, int minor = 0, int patch = 0, string prerelease = "", string build = "")
{
Major = major;
Minor = minor;
Patch = patch;
Prerelease = prerelease ?? "";
Build = build ?? "";
}
/// <summary>
/// Constructs a new instance of the <see cref="SemVersion"/> class from
/// a <see cref="System.Version"/>.
/// </summary>
/// <param name="version">The <see cref="Version"/> that is used to initialize
/// the Major, Minor, Patch and Build.</param>
/// <returns>A <see cref="SemVersion"/> with the same Major and Minor version.
/// The Patch version will be the fourth part of the version number. The
/// build meta data will contain the third part of the version number if
/// it is greater than zero.</returns>
public SemVersion(Version version)
{
if (version == null)
throw new ArgumentNullException(nameof(version));
Major = version.Major;
Minor = version.Minor;
if (version.Revision >= 0)
Patch = version.Revision;
Prerelease = "";
Build = version.Build > 0 ? version.Build.ToString(CultureInfo.InvariantCulture) : "";
}
/// <summary>
/// Converts the string representation of a semantic version to its <see cref="SemVersion"/> equivalent.
/// </summary>
/// <param name="version">The version string.</param>
/// <param name="strict">If set to <see langword="true"/> minor and patch version are required,
/// otherwise they are optional.</param>
/// <returns>The <see cref="SemVersion"/> object.</returns>
/// <exception cref="ArgumentNullException">The <paramref name="version"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">The <paramref name="version"/> has an invalid format.</exception>
/// <exception cref="InvalidOperationException">The <paramref name="version"/> is missing Minor or Patch versions and <paramref name="strict"/> is <see langword="true"/>.</exception>
/// <exception cref="OverflowException">The Major, Minor, or Patch versions are larger than <code>int.MaxValue</code>.</exception>
public static SemVersion Parse(string version, bool strict = false)
{
var match = ParseEx.Match(version);
if (!match.Success)
throw new ArgumentException($"Invalid version '{version}'.", nameof(version));
var major = int.Parse(match.Groups["major"].Value, CultureInfo.InvariantCulture);
var minorMatch = match.Groups["minor"];
int minor = 0;
if (minorMatch.Success)
minor = int.Parse(minorMatch.Value, CultureInfo.InvariantCulture);
else if (strict)
throw new InvalidOperationException("Invalid version (no minor version given in strict mode)");
var patchMatch = match.Groups["patch"];
int patch = 0;
if (patchMatch.Success)
patch = int.Parse(patchMatch.Value, CultureInfo.InvariantCulture);
else if (strict)
throw new InvalidOperationException("Invalid version (no patch version given in strict mode)");
var prerelease = match.Groups["pre"].Value;
var build = match.Groups["build"].Value;
return new SemVersion(major, minor, patch, prerelease, build);
}
/// <summary>
/// Converts the string representation of a semantic version to its <see cref="SemVersion"/>
/// equivalent and returns a value that indicates whether the conversion succeeded.
/// </summary>
/// <param name="version">The version string.</param>
/// <param name="semver">When the method returns, contains a <see cref="SemVersion"/> instance equivalent
/// to the version string passed in, if the version string was valid, or <see langword="null"/> if the
/// version string was not valid.</param>
/// <param name="strict">If set to <see langword="true"/> minor and patch version are required,
/// otherwise they are optional.</param>
/// <returns><see langword="false"/> when a invalid version string is passed, otherwise <see langword="true"/>.</returns>
public static bool TryParse(string version, out SemVersion semver, bool strict = false)
{
semver = null;
if (version is null) return false;
var match = ParseEx.Match(version);
if (!match.Success) return false;
if (!int.TryParse(match.Groups["major"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var major))
return false;
var minorMatch = match.Groups["minor"];
int minor = 0;
if (minorMatch.Success)
{
if (!int.TryParse(minorMatch.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out minor))
return false;
}
else if (strict) return false;
var patchMatch = match.Groups["patch"];
int patch = 0;
if (patchMatch.Success)
{
if (!int.TryParse(patchMatch.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out patch))
return false;
}
else if (strict) return false;
var prerelease = match.Groups["pre"].Value;
var build = match.Groups["build"].Value;
semver = new SemVersion(major, minor, patch, prerelease, build);
return true;
}
/// <summary>
/// Checks whether two semantic versions are equal.
/// </summary>
/// <param name="versionA">The first version to compare.</param>
/// <param name="versionB">The second version to compare.</param>
/// <returns><see langword="true"/> if the two values are equal, otherwise <see langword="false"/>.</returns>
public static bool Equals(SemVersion versionA, SemVersion versionB)
{
if (ReferenceEquals(versionA, versionB)) return true;
if (versionA is null || versionB is null) return false;
return versionA.Equals(versionB);
}
/// <summary>
/// Compares the specified versions.
/// </summary>
/// <param name="versionA">The first version to compare.</param>
/// <param name="versionB">The second version to compare.</param>
/// <returns>A signed number indicating the relative values of <paramref name="versionA"/> and <paramref name="versionB"/>.</returns>
public static int Compare(SemVersion versionA, SemVersion versionB)
{
if (ReferenceEquals(versionA, versionB)) return 0;
if (versionA is null) return -1;
if (versionB is null) return 1;
return versionA.CompareTo(versionB);
}
/// <summary>
/// Make a copy of the current instance with changed properties.
/// </summary>
/// <param name="major">The value to replace the major version or <see langword="null"/> to leave it unchanged.</param>
/// <param name="minor">The value to replace the minor version or <see langword="null"/> to leave it unchanged.</param>
/// <param name="patch">The value to replace the patch version or <see langword="null"/> to leave it unchanged.</param>
/// <param name="prerelease">The value to replace the prerelease version or <see langword="null"/> to leave it unchanged.</param>
/// <param name="build">The value to replace the build metadata or <see langword="null"/> to leave it unchanged.</param>
/// <returns>The new version object.</returns>
/// <remarks>
/// The change method is intended to be called using named argument syntax, passing only
/// those fields to be changed.
/// </remarks>
/// <example>
/// To change only the patch version:
/// <code>version.Change(patch: 4)</code>
/// </example>
public SemVersion Change(int? major = null, int? minor = null, int? patch = null,
string prerelease = null, string build = null)
{
return new SemVersion(
major ?? Major,
minor ?? Minor,
patch ?? Patch,
prerelease ?? Prerelease,
build ?? Build);
}
/// <summary>
/// Gets the major version.
/// </summary>
/// <value>
/// The major version.
/// </value>
public int Major { get; }
/// <summary>
/// Gets the minor version.
/// </summary>
/// <value>
/// The minor version.
/// </value>
public int Minor { get; }
/// <summary>
/// Gets the patch version.
/// </summary>
/// <value>
/// The patch version.
/// </value>
public int Patch { get; }
/// <summary>
/// Gets the prerelease version.
/// </summary>
/// <value>
/// The prerelease version. Empty string if this is a release version.
/// </value>
public string Prerelease { get; }
/// <summary>
/// Gets the build metadata.
/// </summary>
/// <value>
/// The build metadata. Empty string if there is no build metadata.
/// </value>
public string Build { get; }
/// <summary>
/// Returns the <see cref="string" /> equivalent of this version.
/// </summary>
/// <returns>
/// The <see cref="string" /> equivalent of this version.
/// </returns>
public override string ToString()
{
// Assume all separators ("..-+"), at most 2 extra chars
var estimatedLength = 4 + Major.Digits() + Minor.Digits() + Patch.Digits()
+ Prerelease.Length + Build.Length;
var version = new StringBuilder(estimatedLength);
version.Append(Major);
version.Append('.');
version.Append(Minor);
version.Append('.');
version.Append(Patch);
if (Prerelease.Length > 0)
{
version.Append('-');
version.Append(Prerelease);
}
if (Build.Length > 0)
{
version.Append('+');
version.Append(Build);
}
return version.ToString();
}
/// <summary>
/// Compares the current instance with another object of the same type and returns an integer that indicates
/// whether the current instance precedes, follows, or occurs in the same position in the sort order as the
/// other object.
/// </summary>
/// <param name="obj">An object to compare with this instance.</param>
/// <returns>
/// A value that indicates the relative order of the objects being compared.
/// The return value has these meanings:
/// Less than zero: This instance precedes <paramref name="obj" /> in the sort order.
/// Zero: This instance occurs in the same position in the sort order as <paramref name="obj" />.
/// Greater than zero: This instance follows <paramref name="obj" /> in the sort order.
/// </returns>
/// <exception cref="InvalidCastException">The <paramref name="obj"/> is not a <see cref="SemVersion"/>.</exception>
public int CompareTo(object obj)
{
return CompareTo((SemVersion)obj);
}
/// <summary>
/// Compares the current instance with another object of the same type and returns an integer that indicates
/// whether the current instance precedes, follows, or occurs in the same position in the sort order as the
/// other object.
/// </summary>
/// <param name="other">An object to compare with this instance.</param>
/// <returns>
/// A value that indicates the relative order of the objects being compared.
/// The return value has these meanings:
/// Less than zero: This instance precedes <paramref name="other" /> in the sort order.
/// Zero: This instance occurs in the same position in the sort order as <paramref name="other" />.
/// Greater than zero: This instance follows <paramref name="other" /> in the sort order.
/// </returns>
public int CompareTo(SemVersion other)
{
var r = CompareByPrecedence(other);
if (r != 0) return r;
#pragma warning disable CA1062 // Validate arguments of public methods
// If other is null, CompareByPrecedence() returns 1
return CompareComponent(Build, other.Build);
#pragma warning restore CA1062 // Validate arguments of public methods
}
/// <summary>
/// Returns whether two semantic versions have the same precedence. Versions
/// that differ only by build metadata have the same precedence.
/// </summary>
/// <param name="other">The semantic version to compare to.</param>
/// <returns><see langword="true"/> if the version precedences are equal.</returns>
public bool PrecedenceMatches(SemVersion other)
{
return CompareByPrecedence(other) == 0;
}
/// <summary>
/// Compares two semantic versions by precedence as defined in the SemVer spec. Versions
/// that differ only by build metadata have the same precedence.
/// </summary>
/// <param name="other">The semantic version.</param>
/// <returns>
/// A value that indicates the relative order of the objects being compared.
/// The return value has these meanings:
/// Less than zero: This instance precedes <paramref name="other" /> in the sort order.
/// Zero: This instance occurs in the same position in the sort order as <paramref name="other" />.
/// Greater than zero: This instance follows <paramref name="other" /> in the sort order.
/// </returns>
public int CompareByPrecedence(SemVersion other)
{
if (other is null)
return 1;
var r = Major.CompareTo(other.Major);
if (r != 0) return r;
r = Minor.CompareTo(other.Minor);
if (r != 0) return r;
r = Patch.CompareTo(other.Patch);
if (r != 0) return r;
return CompareComponent(Prerelease, other.Prerelease, true);
}
private static int CompareComponent(string a, string b, bool nonemptyIsLower = false)
{
var aEmpty = string.IsNullOrEmpty(a);
var bEmpty = string.IsNullOrEmpty(b);
if (aEmpty && bEmpty)
return 0;
if (aEmpty)
return nonemptyIsLower ? 1 : -1;
if (bEmpty)
return nonemptyIsLower ? -1 : 1;
var aComps = a.Split('.');
var bComps = b.Split('.');
var minLen = Math.Min(aComps.Length, bComps.Length);
for (int i = 0; i < minLen; i++)
{
var ac = aComps[i];
var bc = bComps[i];
var aIsNum = int.TryParse(ac, out var aNum);
var bIsNum = int.TryParse(bc, out var bNum);
int r;
if (aIsNum && bIsNum)
{
r = aNum.CompareTo(bNum);
if (r != 0) return r;
}
else
{
if (aIsNum)
return -1;
if (bIsNum)
return 1;
r = string.CompareOrdinal(ac, bc);
if (r != 0)
return r;
}
}
return aComps.Length.CompareTo(bComps.Length);
}
/// <summary>
/// Determines whether the specified <see cref="object" /> is equal to this instance.
/// </summary>
/// <param name="obj">The <see cref="object" /> to compare with this instance.</param>
/// <returns>
/// <see langword="true"/> if the specified <see cref="object" /> is equal to this instance, otherwise <see langword="false"/>.
/// </returns>
/// <exception cref="InvalidCastException">The <paramref name="obj"/> is not a <see cref="SemVersion"/>.</exception>
public override bool Equals(object obj)
{
if (obj is null)
return false;
if (ReferenceEquals(this, obj))
return true;
var other = (SemVersion)obj;
return Major == other.Major
&& Minor == other.Minor
&& Patch == other.Patch
&& string.Equals(Prerelease, other.Prerelease, StringComparison.Ordinal)
&& string.Equals(Build, other.Build, StringComparison.Ordinal);
}
/// <summary>
/// Returns a hash code for this instance.
/// </summary>
/// <returns>
/// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.
/// </returns>
public override int GetHashCode()
{
unchecked
{
// TODO verify this. Some versions start result = 17. Some use 37 instead of 31
int result = Major.GetHashCode();
result = result * 31 + Minor.GetHashCode();
result = result * 31 + Patch.GetHashCode();
result = result * 31 + Prerelease.GetHashCode();
result = result * 31 + Build.GetHashCode();
return result;
}
}
#if !NETSTANDARD
/// <summary>
/// Populates a <see cref="SerializationInfo"/> with the data needed to serialize the target object.
/// </summary>
/// <param name="info">The <see cref="SerializationInfo"/> to populate with data.</param>
/// <param name="context">The destination (see <see cref="SerializationInfo"/>) for this serialization.</param>
[SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
if (info == null) throw new ArgumentNullException(nameof(info));
info.AddValue("SemVersion", ToString());
}
#endif
#pragma warning disable CA2225 // Operator overloads have named alternates
/// <summary>
/// Implicit conversion from <see cref="string"/> to <see cref="SemVersion"/>.
/// </summary>
/// <param name="version">The semantic version.</param>
/// <returns>The <see cref="SemVersion"/> object.</returns>
/// <exception cref="ArgumentNullException">The <paramref name="version"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">The version number has an invalid format.</exception>
/// <exception cref="OverflowException">The Major, Minor, or Patch versions are larger than <code>int.MaxValue</code>.</exception>
public static implicit operator SemVersion(string version)
#pragma warning restore CA2225 // Operator overloads have named alternates
{
return Parse(version);
}
/// <summary>
/// Compares two semantic versions for equality.
/// </summary>
/// <param name="left">The left value.</param>
/// <param name="right">The right value.</param>
/// <returns>If left is equal to right <see langword="true"/>, otherwise <see langword="false"/>.</returns>
public static bool operator ==(SemVersion left, SemVersion right)
{
return Equals(left, right);
}
/// <summary>
/// Compares two semantic versions for inequality.
/// </summary>
/// <param name="left">The left value.</param>
/// <param name="right">The right value.</param>
/// <returns>If left is not equal to right <see langword="true"/>, otherwise <see langword="false"/>.</returns>
public static bool operator !=(SemVersion left, SemVersion right)
{
return !Equals(left, right);
}
/// <summary>
/// Compares two semantic versions.
/// </summary>
/// <param name="left">The left value.</param>
/// <param name="right">The right value.</param>
/// <returns>If left is greater than right <see langword="true"/>, otherwise <see langword="false"/>.</returns>
public static bool operator >(SemVersion left, SemVersion right)
{
return Compare(left, right) > 0;
}
/// <summary>
/// Compares two semantic versions.
/// </summary>
/// <param name="left">The left value.</param>
/// <param name="right">The right value.</param>
/// <returns>If left is greater than or equal to right <see langword="true"/>, otherwise <see langword="false"/>.</returns>
public static bool operator >=(SemVersion left, SemVersion right)
{
return Equals(left, right) || Compare(left, right) > 0;
}
/// <summary>
/// Compares two semantic versions.
/// </summary>
/// <param name="left">The left value.</param>
/// <param name="right">The right value.</param>
/// <returns>If left is less than right <see langword="true"/>, otherwise <see langword="false"/>.</returns>
public static bool operator <(SemVersion left, SemVersion right)
{
return Compare(left, right) < 0;
}
/// <summary>
/// Compares two semantic versions.
/// </summary>
/// <param name="left">The left value.</param>
/// <param name="right">The right value.</param>
/// <returns>If left is less than or equal to right <see langword="true"/>, otherwise <see langword="false"/>.</returns>
public static bool operator <=(SemVersion left, SemVersion right)
{
return Equals(left, right) || Compare(left, right) < 0;
}
}
}