// SPDX-License-Identifier: MIT /** * Digital Voice Modem - Fixed Network Equipment Core Library * AGPLv3 Open Source. Use is subject to license terms. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * @package DVM / Fixed Network Equipment Core Library * @derivedfrom A Semantic Version Library for .Net project. (https://github.com/maxhauser/semver) * @license MIT License (https://opensource.org/licenses/MIT) * * Copyright (c) 2013 Max Hauser * Copyright (C) 2022 Bryan Biedenkapp, N2PLL * */ using System; using System.Globalization; using System.Text; using System.Reflection; using System.Runtime.Serialization; using System.Security.Permissions; using System.Text.RegularExpressions; namespace fnecore.Utility { /// /// /// internal static class IntExtensions { /// /// The number of digits in a non-negative number. Returns 1 for all /// negative numbers. That is ok because we are using it to calculate /// string length for a for numbers that /// aren't supposed to be negative, but when they are it is just a little /// slower. /// /// /// This approach is based on https://stackoverflow.com/a/51099524/268898 /// where the poster offers performance benchmarks showing this is the /// fastest way to get a number of digits. /// public static int Digits(this int n) { if (n < 10) return 1; if (n < 100) return 2; if (n < 1_000) return 3; if (n < 10_000) return 4; if (n < 100_000) return 5; if (n < 1_000_000) return 6; if (n < 10_000_000) return 7; if (n < 100_000_000) return 8; if (n < 1_000_000_000) return 9; return 10; } } // internal static class IntExtensions /// /// A semantic version implementation. /// Conforms with v2.0.0 of http://semver.org /// [Serializable] public sealed class SemVersion : IComparable, IComparable, ISerializable { private static readonly Regex ParseEx = new Regex(@"^(?\d+)" + @"(?>\.(?\d+))?" + @"(?>\.(?\d+))?" + @"(?>\-(?
[0-9A-Za-z\-\.]+))?" + @"(?>\+(?[0-9A-Za-z\-\.]+))?$",
                RegexOptions.CultureInvariant | RegexOptions.Compiled | RegexOptions.ExplicitCapture,
                TimeSpan.FromSeconds(0.5));

        /*
        ** Properties
        */

        /// 
        /// Gets the major version.
        /// 
        /// 
        /// The major version.
        /// 
        public int Major { get; }

        /// 
        /// Gets the minor version.
        /// 
        /// 
        /// The minor version.
        /// 
        public int Minor { get; }

        /// 
        /// Gets the patch version.
        /// 
        /// 
        /// The patch version.
        /// 
        public int Patch { get; }

        /// 
        /// Gets the prerelease version.
        /// 
        /// 
        /// The prerelease version. Empty string if this is a release version.
        /// 
        public string Prerelease { get; }

        /// 
        /// Gets the build metadata.
        /// 
        /// 
        /// The build metadata. Empty string if there is no build metadata.
        /// 
        public string Build { get; }

        /*
        ** Operators
        */

#pragma warning disable CA2225 // Operator overloads have named alternates
        /// 
        /// Implicit conversion from  to .
        /// 
        /// The semantic version.
        /// The  object.
        /// The  is .
        /// The version number has an invalid format.
        /// The Major, Minor, or Patch versions are larger than int.MaxValue.
        public static implicit operator SemVersion(string version)
#pragma warning restore CA2225 // Operator overloads have named alternates
        {
            return Parse(version);
        }

        /// 
        /// Compares two semantic versions for equality.
        /// 
        /// The left value.
        /// The right value.
        /// If left is equal to right , otherwise .
        public static bool operator ==(SemVersion left, SemVersion right)
        {
            return Equals(left, right);
        }

        /// 
        /// Compares two semantic versions for inequality.
        /// 
        /// The left value.
        /// The right value.
        /// If left is not equal to right , otherwise .
        public static bool operator !=(SemVersion left, SemVersion right)
        {
            return !Equals(left, right);
        }

        /// 
        /// Compares two semantic versions.
        /// 
        /// The left value.
        /// The right value.
        /// If left is greater than right , otherwise .
        public static bool operator >(SemVersion left, SemVersion right)
        {
            return Compare(left, right) > 0;
        }

        /// 
        /// Compares two semantic versions.
        /// 
        /// The left value.
        /// The right value.
        /// If left is greater than or equal to right , otherwise .
        public static bool operator >=(SemVersion left, SemVersion right)
        {
            return Equals(left, right) || Compare(left, right) > 0;
        }

        /// 
        /// Compares two semantic versions.
        /// 
        /// The left value.
        /// The right value.
        /// If left is less than right , otherwise .
        public static bool operator <(SemVersion left, SemVersion right)
        {
            return Compare(left, right) < 0;
        }

        /// 
        /// Compares two semantic versions.
        /// 
        /// The left value.
        /// The right value.
        /// If left is less than or equal to right , otherwise .
        public static bool operator <=(SemVersion left, SemVersion right)
        {
            return Equals(left, right) || Compare(left, right) < 0;
        }

        /*
        ** Methods
        */

#pragma warning disable CA1801 // Parameter unused
        /// 
        /// Deserialize a .
        /// 
        /// The  parameter is null.
        private SemVersion(SerializationInfo info, StreamingContext context)
#pragma warning restore CA1801 // Parameter unused
        {
            if (info == null) throw new ArgumentNullException(nameof(info));
            SemVersion semVersion = Parse(info.GetString("SemVersion"));
            Major = semVersion.Major;
            Minor = semVersion.Minor;
            Patch = semVersion.Patch;
            Prerelease = semVersion.Prerelease;
            Build = semVersion.Build;
        }

        /// 
        /// Initializes a new instance of the  class.
        /// 
        /// The major version.
        /// The minor version.
        /// The patch version.
        /// The prerelease version (e.g. "alpha").
        /// The build metadata (e.g. "nightly.232").
        public SemVersion(int major, int minor = 0, int patch = 0, string prerelease = "", string build = "")
        {
            Major = major;
            Minor = minor;
            Patch = patch;

            Prerelease = prerelease ?? "";
            Build = build ?? "";
        }

        /// 
        /// Initializes a new instance of the  class from
        /// a .
        /// 
        /// The  that is used to initialize
        /// the Major, Minor, Patch and Build.
        /// The prerelease version (e.g. "alpha").
        /// A  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.
        public SemVersion(Assembly assembly, string prerelease = "")
        {
            if (assembly == null)
                throw new ArgumentNullException(nameof(assembly));

            Version version = assembly.GetName().Version;

            Major = version.Major;
            Minor = version.Minor;

            if (version.Revision >= 0)
                Patch = version.Revision;

            Prerelease = prerelease;

            Build = version.Build > 0 ? version.Build.ToString(CultureInfo.InvariantCulture) : "";
        }

        /// 
        /// Initializes a new instance of the  class from
        /// a .
        /// 
        /// The  that is used to initialize
        /// the Major, Minor, Patch and Build.
        /// The prerelease version (e.g. "alpha").
        /// A  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.
        public SemVersion(Version version, string prerelease = "")
        {
            if (version == null)
                throw new ArgumentNullException(nameof(version));

            Major = version.Major;
            Minor = version.Minor;

            if (version.Revision >= 0)
                Patch = version.Revision;

            Prerelease = prerelease;

            Build = version.Build > 0 ? version.Build.ToString(CultureInfo.InvariantCulture) : "";
        }

        /// 
        /// Converts the string representation of a semantic version to its  equivalent.
        /// 
        /// The version string.
        /// If set to  minor and patch version are required,
        /// otherwise they are optional.
        /// The  object.
        /// The  is .
        /// The  has an invalid format.
        /// The  is missing Minor or Patch versions and  is .
        /// The Major, Minor, or Patch versions are larger than int.MaxValue.
        public static SemVersion Parse(string version, bool strict = false)
        {
            Match match = ParseEx.Match(version);
            if (!match.Success)
                throw new ArgumentException($"Invalid version '{version}'.", nameof(version));

            int major = int.Parse(match.Groups["major"].Value, CultureInfo.InvariantCulture);

            Group 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)");

            Group 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)");

            string prerelease = match.Groups["pre"].Value;
            string build = match.Groups["build"].Value;

            return new SemVersion(major, minor, patch, prerelease, build);
        }

        /// 
        /// Converts the string representation of a semantic version to its 
        /// equivalent and returns a value that indicates whether the conversion succeeded.
        /// 
        /// The version string.
        /// When the method returns, contains a  instance equivalent
        /// to the version string passed in, if the version string was valid, or  if the
        /// version string was not valid.
        /// If set to  minor and patch version are required,
        /// otherwise they are optional.
        ///  when a invalid version string is passed, otherwise .
        public static bool TryParse(string version, out SemVersion semver, bool strict = false)
        {
            semver = null;
            if (version is null) 
                return false;

            Match 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;

            Group 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;

            Group 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;

            string prerelease = match.Groups["pre"].Value;
            string build = match.Groups["build"].Value;

            semver = new SemVersion(major, minor, patch, prerelease, build);
            return true;
        }

        /// 
        /// Checks whether two semantic versions are equal.
        /// 
        /// The first version to compare.
        /// The second version to compare.
        ///  if the two values are equal, otherwise .
        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);
        }

        /// 
        /// Compares the specified versions.
        /// 
        /// The first version to compare.
        /// The second version to compare.
        /// A signed number indicating the relative values of  and .
        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);
        }

        /// 
        /// Make a copy of the current instance with changed properties.
        /// 
        /// The value to replace the major version or  to leave it unchanged.
        /// The value to replace the minor version or  to leave it unchanged.
        /// The value to replace the patch version or  to leave it unchanged.
        /// The value to replace the prerelease version or  to leave it unchanged.
        /// The value to replace the build metadata or  to leave it unchanged.
        /// The new version object.
        /// 
        /// The change method is intended to be called using named argument syntax, passing only
        /// those fields to be changed.
        /// 
        /// 
        /// To change only the patch version:
        /// version.Change(patch: 4)
        /// 
        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);
        }

        /// 
        /// Returns the  equivalent of this version.
        /// 
        /// 
        /// The  equivalent of this version.
        /// 
        public override string ToString()
        {
            // Assume all separators ("..-+"), at most 2 extra chars
            int estimatedLength = 4 + Major.Digits() + Minor.Digits() + Patch.Digits() + Prerelease.Length + Build.Length;
            StringBuilder 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();
        }

        /// 
        /// 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.
        /// 
        /// An object to compare with this instance.
        /// 
        /// A value that indicates the relative order of the objects being compared.
        /// The return value has these meanings:
        ///  Less than zero: This instance precedes  in the sort order.
        ///  Zero: This instance occurs in the same position in the sort order as .
        ///  Greater than zero: This instance follows  in the sort order.
        /// 
        /// The  is not a .
        public int CompareTo(object obj)
        {
            return CompareTo((SemVersion)obj);
        }

        /// 
        /// 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.
        /// 
        /// An object to compare with this instance.
        /// 
        /// A value that indicates the relative order of the objects being compared.
        /// The return value has these meanings:
        ///  Less than zero: This instance precedes  in the sort order.
        ///  Zero: This instance occurs in the same position in the sort order as .
        ///  Greater than zero: This instance follows  in the sort order.
        /// 
        public int CompareTo(SemVersion other)
        {
            int 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
        }

        /// 
        /// Returns whether two semantic versions have the same precedence. Versions
        /// that differ only by build metadata have the same precedence.
        /// 
        /// The semantic version to compare to.
        ///  if the version precedences are equal.
        public bool PrecedenceMatches(SemVersion other)
        {
            return CompareByPrecedence(other) == 0;
        }

        /// 
        /// Compares two semantic versions by precedence as defined in the SemVer spec. Versions
        /// that differ only by build metadata have the same precedence.
        /// 
        /// The semantic version.
        /// 
        /// A value that indicates the relative order of the objects being compared.
        /// The return value has these meanings:
        ///  Less than zero: This instance precedes  in the sort order.
        ///  Zero: This instance occurs in the same position in the sort order as .
        ///  Greater than zero: This instance follows  in the sort order.
        /// 
        public int CompareByPrecedence(SemVersion other)
        {
            if (other is null)
                return 1;

            int 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)
        {
            bool aEmpty = string.IsNullOrEmpty(a);
            bool bEmpty = string.IsNullOrEmpty(b);
            if (aEmpty && bEmpty)
                return 0;

            if (aEmpty)
                return nonemptyIsLower ? 1 : -1;
            if (bEmpty)
                return nonemptyIsLower ? -1 : 1;

            string[] aComps = a.Split('.');
            string[] bComps = b.Split('.');

            int minLen = Math.Min(aComps.Length, bComps.Length);
            for (int i = 0; i < minLen; i++)
            {
                string ac = aComps[i];
                string bc = bComps[i];
                bool aIsNum = int.TryParse(ac, out var aNum);
                bool 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);
        }

        /// 
        /// Determines whether the specified  is equal to this instance.
        /// 
        /// The  to compare with this instance.
        /// 
        ///    if the specified  is equal to this instance, otherwise .
        /// 
        /// The  is not a .
        public override bool Equals(object obj)
        {
            if (obj is null)
                return false;

            if (ReferenceEquals(this, obj))
                return true;

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

        /// 
        /// Returns a hash code for this instance.
        /// 
        /// 
        /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.
        /// 
        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;
            }
        }

        /// 
        /// Populates a  with the data needed to serialize the target object.
        /// 
        /// The  to populate with data.
        /// The destination (see ) for this serialization.
        [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
        public void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            if (info == null)
                throw new ArgumentNullException(nameof(info));
            info.AddValue("SemVersion", ToString());
        }
    } // public sealed class SemVersion : IComparable, IComparable, ISerializable
} // namespace fnecore.Utility