File: Utilities\SimpleVersion.cs
Web Access
Project: ..\..\..\src\Build\Microsoft.Build.csproj (Microsoft.Build)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Diagnostics;
using System.Globalization;
using Microsoft.Build.Shared;
 
#nullable disable
 
namespace Microsoft.Build.Utilities
{
    /// <summary>
    /// Simple replacement for System.Version used to implement version
    /// comparison intrinic property functions.
    ///
    /// Allows major version only (e.g. "3" is 3.0.0.0), ignores leading 'v'
    /// (e.g. "v3.0" is 3.0.0.0).
    ///
    /// Ignores semver prerelease and metadata portions (e.g. "1.0.0-preview+info"
    /// is 1.0.0.0).
    ///
    /// Treats unspecified components as 0 (e.g. x == x.0 == x.0.0 == x.0.0.0).
    ///
    /// Ignores leading and trailing whitespace, but does not tolerate whitespace
    /// between components, unlike System.Version.
    ///
    /// Also unlike System.Version, '+' is ignored as semver metadata as described
    /// above, not tolerated as positive sign of integer component.
    /// </summary>
    /// <remarks>
    /// Tolerating leading 'v' allows using $(TargetFrameworkVersion) directly.
    ///
    /// Ignoring semver portions allows, for example, checking >= major.minor
    /// while still in development of that release.
    ///
    /// Implemented as a struct to avoid heap allocation. Parsing is done
    /// without heap allocation at all on .NET Core. However, on .NET Framework,
    /// the integer component substrings are allocated as there is no int.Parse
    /// on span there.
    /// </remarks>
    internal readonly struct SimpleVersion : IEquatable<SimpleVersion>, IComparable<SimpleVersion>
    {
        public readonly int Major;
        public readonly int Minor;
        public readonly int Build;
        public readonly int Revision;
 
        public SimpleVersion(int major, int minor = 0, int build = 0, int revision = 0)
        {
            if (major < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(major));
            }
 
            if (minor < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(minor));
            }
 
            if (build < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(build));
            }
 
            if (revision < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(revision));
            }
 
            Major = major;
            Minor = minor;
            Build = build;
            Revision = revision;
        }
 
        public bool Equals(SimpleVersion other)
        {
            return Major == other.Major &&
                   Minor == other.Minor &&
                   Build == other.Build &&
                   Revision == other.Revision;
        }
 
        public int CompareTo(SimpleVersion other)
        {
            return Major != other.Major ? (Major > other.Major ? 1 : -1) :
                   Minor != other.Minor ? (Minor > other.Minor ? 1 : -1) :
                   Build != other.Build ? (Build > other.Build ? 1 : -1) :
                   Revision != other.Revision ? (Revision > other.Revision ? 1 : -1) :
                   0;
        }
 
        public override bool Equals(object obj) => obj is SimpleVersion v && Equals(v);
        public override int GetHashCode() => (Major, Minor, Build, Revision).GetHashCode();
        public override string ToString() => FormattableString.Invariant($"{Major}.{Minor}.{Build}.{Revision}");
 
        public static bool operator ==(SimpleVersion a, SimpleVersion b) => a.Equals(b);
        public static bool operator !=(SimpleVersion a, SimpleVersion b) => !a.Equals(b);
        public static bool operator <(SimpleVersion a, SimpleVersion b) => a.CompareTo(b) < 0;
        public static bool operator <=(SimpleVersion a, SimpleVersion b) => a.CompareTo(b) <= 0;
        public static bool operator >(SimpleVersion a, SimpleVersion b) => a.CompareTo(b) > 0;
        public static bool operator >=(SimpleVersion a, SimpleVersion b) => a.CompareTo(b) >= 0;
 
        public static SimpleVersion Parse(string input)
        {
            if (input == null)
            {
                throw new ArgumentNullException(nameof(input));
            }
 
            var span = RemoveTrivia(input);
 
            int minor = 0, build = 0, revision = 0;
 
            if (ParseComponent(ref span, out int major) &&
                ParseComponent(ref span, out minor) &&
                ParseComponent(ref span, out build) &&
                ParseComponent(ref span, out revision))
            {
                // More than 4 components (too many dots)
                throw InvalidVersionFormat();
            }
 
            return new SimpleVersion(major, minor, build, revision);
        }
 
        private static readonly char[] s_semverSeparators = ['-', '+'];
 
        private static ReadOnlySpan<char> RemoveTrivia(string input)
        {
            // Ignore leading/trailing whitespace in input.
            ReadOnlySpan<char> span = input.AsSpan().Trim();
 
            // Ignore a leading "v".
            if (span.Length > 0 && (span[0] == 'v' || span[0] == 'V'))
            {
                span = span.Slice(1);
            }
 
            // Ignore semver separator and anything after.
            int separatorIndex = span.IndexOfAny(s_semverSeparators);
            if (separatorIndex >= 0)
            {
                span = span.Slice(0, separatorIndex);
            }
 
            return span;
        }
 
        private static bool ParseComponent(ref ReadOnlySpan<char> span, out int value)
        {
            int dotIndex = span.IndexOf('.');
            if (dotIndex < 0)
            {
                value = ParseComponent(span);
                return false;
            }
            else
            {
                value = ParseComponent(span.Slice(0, dotIndex));
                span = span.Slice(dotIndex + 1);
                return true;
            }
        }
 
        private static int ParseComponent(ReadOnlySpan<char> span)
        {
#if NETFRAMEWORK
            // Cannot parse int from span on .NET Framework, so allocate the substring
            var spanOrString = span.ToString();
#else
            var spanOrString = span;
#endif
 
            if (!int.TryParse(spanOrString, NumberStyles.None, CultureInfo.InvariantCulture, out int value))
            {
                throw InvalidVersionFormat();
            }
 
            // Cannot parse as negative using NumberStyles.None. Also, +/- would have
            // been stripped as semver trivia earlier.
            Debug.Assert(value >= 0);
 
            return value;
        }
 
        private static Exception InvalidVersionFormat()
        {
            return new FormatException(ResourceUtilities.GetResourceString(nameof(InvalidVersionFormat)));
        }
    }
}