File: src\libraries\System.Private.CoreLib\src\System\Version.cs
Web Access
Project: src\src\coreclr\System.Private.CoreLib\System.Private.CoreLib.csproj (System.Private.CoreLib)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Buffers.Text;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
 
namespace System
{
    // A Version object contains four hierarchical numeric components: major, minor,
    // build and revision.  Build and revision may be unspecified, which is represented
    // internally as a -1.  By definition, an unspecified component matches anything
    // (both unspecified and specified), and an unspecified component is "less than" any
    // specified component.
 
    [Serializable]
    [TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")]
    public sealed class Version : ICloneable, IComparable, IComparable<Version?>, IEquatable<Version?>, ISpanFormattable, IUtf8SpanFormattable, IUtf8SpanParsable<Version>
    {
        // AssemblyName depends on the order staying the same
        private readonly int _Major; // Do not rename (binary serialization)
        private readonly int _Minor; // Do not rename (binary serialization)
        private readonly int _Build; // Do not rename (binary serialization)
        private readonly int _Revision; // Do not rename (binary serialization)
 
        public Version(int major, int minor, int build, int revision)
        {
            ArgumentOutOfRangeException.ThrowIfNegative(major);
            ArgumentOutOfRangeException.ThrowIfNegative(minor);
            ArgumentOutOfRangeException.ThrowIfNegative(build);
            ArgumentOutOfRangeException.ThrowIfNegative(revision);
 
            _Major = major;
            _Minor = minor;
            _Build = build;
            _Revision = revision;
        }
 
        public Version(int major, int minor, int build)
        {
            ArgumentOutOfRangeException.ThrowIfNegative(major);
            ArgumentOutOfRangeException.ThrowIfNegative(minor);
            ArgumentOutOfRangeException.ThrowIfNegative(build);
 
            _Major = major;
            _Minor = minor;
            _Build = build;
            _Revision = -1;
        }
 
        public Version(int major, int minor)
        {
            ArgumentOutOfRangeException.ThrowIfNegative(major);
            ArgumentOutOfRangeException.ThrowIfNegative(minor);
 
            _Major = major;
            _Minor = minor;
            _Build = -1;
            _Revision = -1;
        }
 
        public Version(string version)
        {
            Version v = Parse(version);
            _Major = v.Major;
            _Minor = v.Minor;
            _Build = v.Build;
            _Revision = v.Revision;
        }
 
        public Version()
        {
            //_Major = 0;
            //_Minor = 0;
            _Build = -1;
            _Revision = -1;
        }
 
        private Version(Version version)
        {
            Debug.Assert(version != null);
 
            _Major = version._Major;
            _Minor = version._Minor;
            _Build = version._Build;
            _Revision = version._Revision;
        }
 
        public object Clone()
        {
            return new Version(this);
        }
 
        // Properties for setting and getting version numbers
        public int Major => _Major;
 
        public int Minor => _Minor;
 
        public int Build => _Build;
 
        public int Revision => _Revision;
 
        public short MajorRevision => (short)(_Revision >> 16);
 
        public short MinorRevision => (short)(_Revision & 0xFFFF);
 
        public int CompareTo(object? version)
        {
            if (version == null)
            {
                return 1;
            }
 
            if (version is Version v)
            {
                return CompareTo(v);
            }
 
            throw new ArgumentException(SR.Arg_MustBeVersion, nameof(version));
        }
 
        public int CompareTo(Version? value)
        {
            return
                ReferenceEquals(value, this) ? 0 :
                value is null ? 1 :
                _Major != value._Major ? (_Major > value._Major ? 1 : -1) :
                _Minor != value._Minor ? (_Minor > value._Minor ? 1 : -1) :
                _Build != value._Build ? (_Build > value._Build ? 1 : -1) :
                _Revision != value._Revision ? (_Revision > value._Revision ? 1 : -1) :
                0;
        }
 
        public override bool Equals([NotNullWhen(true)] object? obj)
        {
            return Equals(obj as Version);
        }
 
        public bool Equals([NotNullWhen(true)] Version? obj)
        {
            return ReferenceEquals(obj, this) ||
                (obj is not null &&
                _Major == obj._Major &&
                _Minor == obj._Minor &&
                _Build == obj._Build &&
                _Revision == obj._Revision);
        }
 
        public override int GetHashCode()
        {
            // Let's assume that most version numbers will be pretty small and just
            // OR some lower order bits together.
 
            int accumulator = 0;
 
            accumulator |= (_Major & 0x0000000F) << 28;
            accumulator |= (_Minor & 0x000000FF) << 20;
            accumulator |= (_Build & 0x000000FF) << 12;
            accumulator |= (_Revision & 0x00000FFF);
 
            return accumulator;
        }
 
        public override string ToString() =>
            ToString(DefaultFormatFieldCount);
 
        public unsafe string ToString(int fieldCount)
        {
            Span<char> dest = stackalloc char[(4 * Number.Int32NumberBufferLength) + 3]; // at most 4 Int32s and 3 periods
            bool success = TryFormat(dest, fieldCount, out int charsWritten);
            Debug.Assert(success);
            return dest.Slice(0, charsWritten).ToString();
        }
 
        string IFormattable.ToString(string? format, IFormatProvider? formatProvider) =>
            ToString();
 
        public bool TryFormat(Span<char> destination, out int charsWritten) =>
            TryFormatCore(destination, DefaultFormatFieldCount, out charsWritten);
 
        public bool TryFormat(Span<char> destination, int fieldCount, out int charsWritten) =>
            TryFormatCore(destination, fieldCount, out charsWritten);
 
        /// <summary>Tries to format this version instance into a span of bytes.</summary>
        /// <param name="utf8Destination">The span in which to write this instance's value formatted as a span of UTF-8 bytes.</param>
        /// <param name="bytesWritten">When this method returns, contains the number of bytes that were written in <paramref name="utf8Destination"/>.</param>
        /// <returns><see langword="true"/> if the formatting was successful; otherwise, <see langword="false"/>.</returns>
        public bool TryFormat(Span<byte> utf8Destination, out int bytesWritten) =>
            TryFormatCore(utf8Destination, DefaultFormatFieldCount, out bytesWritten);
 
        /// <summary>Tries to format this version instance into a span of bytes.</summary>
        /// <param name="utf8Destination">The span in which to write this instance's value formatted as a span of UTF-8 bytes.</param>
        /// <param name="fieldCount">The number of components to return. This value ranges from 0 to 4.</param>
        /// <param name="bytesWritten">When this method returns, contains the number of bytes that were written in <paramref name="utf8Destination"/>.</param>
        /// <returns><see langword="true"/> if the formatting was successful; otherwise, <see langword="false"/>.</returns>
        public bool TryFormat(Span<byte> utf8Destination, int fieldCount, out int bytesWritten) =>
            TryFormatCore(utf8Destination, fieldCount, out bytesWritten);
 
        private bool TryFormatCore<TChar>(Span<TChar> destination, int fieldCount, out int charsWritten) where TChar : unmanaged, IUtfChar<TChar>
        {
            Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));
 
            switch ((uint)fieldCount)
            {
                case > 4:
                    ThrowArgumentException("4");
                    break;
 
                case >= 3 when _Build == -1:
                    ThrowArgumentException("2");
                    break;
 
                case 4 when _Revision == -1:
                    ThrowArgumentException("3");
                    break;
 
                static void ThrowArgumentException(string failureUpperBound) =>
                    throw new ArgumentException(SR.Format(SR.ArgumentOutOfRange_Bounds_Lower_Upper, "0", failureUpperBound), nameof(fieldCount));
            }
 
            int totalCharsWritten = 0;
 
            for (int i = 0; i < fieldCount; i++)
            {
                if (i != 0)
                {
                    if (destination.IsEmpty)
                    {
                        charsWritten = 0;
                        return false;
                    }
 
                    destination[0] = TChar.CastFrom('.');
                    destination = destination.Slice(1);
                    totalCharsWritten++;
                }
 
                int value = i switch
                {
                    0 => _Major,
                    1 => _Minor,
                    2 => _Build,
                    _ => _Revision
                };
 
                int valueCharsWritten;
                bool formatted = typeof(TChar) == typeof(char) ?
                    ((uint)value).TryFormat(Unsafe.BitCast<Span<TChar>, Span<char>>(destination), out valueCharsWritten) :
                    ((uint)value).TryFormat(Unsafe.BitCast<Span<TChar>, Span<byte>>(destination), out valueCharsWritten, default, CultureInfo.InvariantCulture);
 
                if (!formatted)
                {
                    charsWritten = 0;
                    return false;
                }
 
                totalCharsWritten += valueCharsWritten;
                destination = destination.Slice(valueCharsWritten);
            }
 
            charsWritten = totalCharsWritten;
            return true;
        }
 
        bool ISpanFormattable.TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider? provider) =>
            // format and provider are ignored.
            TryFormatCore(destination, DefaultFormatFieldCount, out charsWritten);
 
        /// <inheritdoc cref="IUtf8SpanFormattable.TryFormat" />
        bool IUtf8SpanFormattable.TryFormat(Span<byte> utf8Destination, out int bytesWritten, ReadOnlySpan<char> format, IFormatProvider? provider) =>
            // format and provider are ignored.
            TryFormatCore(utf8Destination, DefaultFormatFieldCount, out bytesWritten);
 
        private int DefaultFormatFieldCount =>
            _Build == -1 ? 2 :
            _Revision == -1 ? 3 :
            4;
 
        public static Version Parse(string input)
        {
            ArgumentNullException.ThrowIfNull(input);
 
            return ParseVersion(input.AsSpan(), throwOnFailure: true)!;
        }
 
        public static Version Parse(ReadOnlySpan<char> input) =>
            ParseVersion(input, throwOnFailure: true)!;
 
        /// <inheritdoc cref="IUtf8SpanParsable{TSelf}.Parse(ReadOnlySpan{byte}, IFormatProvider?)"/>
        static Version IUtf8SpanParsable<Version>.Parse(ReadOnlySpan<byte> utf8Text, IFormatProvider? provider)
        {
            Version? result = ParseVersion(utf8Text, throwOnFailure: false);
            // Required to throw FormatException for invalid input according to contract.
            if (result == null)
            {
                ThrowHelper.ThrowFormatInvalidString();
            }
            return result;
        }
 
        /// <summary>
        /// Converts the specified read-only span of UTF-8 characters that represents a version number to an equivalent Version object.
        /// </summary>
        /// <param name="utf8Text">A read-only span of UTF-8 characters that contains a version number to convert.</param>
        /// <returns>An object that is equivalent to the version number specified in the <paramref name="utf8Text" /> parameter.</returns>
        /// <exception cref="ArgumentException"><paramref name="utf8Text" /> has fewer than two or more than four version components.</exception>
        /// <exception cref="ArgumentOutOfRangeException">At least one component in <paramref name="utf8Text" /> is less than zero.</exception>
        /// <exception cref="FormatException">At least one component in <paramref name="utf8Text" /> is not an integer.</exception>
        /// <exception cref="OverflowException">At least one component in <paramref name="utf8Text" /> represents a number that is greater than <see cref="int.MaxValue"/>.</exception>
        public static Version Parse(ReadOnlySpan<byte> utf8Text) =>
            ParseVersion(utf8Text, throwOnFailure: true)!;
 
        public static bool TryParse([NotNullWhen(true)] string? input, [NotNullWhen(true)] out Version? result)
        {
            if (input == null)
            {
                result = null;
                return false;
            }
 
            result = ParseVersion(input.AsSpan(), throwOnFailure: false);
            return result is not null;
        }
 
        public static bool TryParse(ReadOnlySpan<char> input, [NotNullWhen(true)] out Version? result)
        {
            result = ParseVersion(input, throwOnFailure: false);
            return result is not null;
        }
 
        /// <summary>
        /// Tries to convert the UTF-8 representation of a version number to an equivalent Version object, and returns a value that indicates whether the conversion succeeded.
        /// </summary>
        /// <param name="utf8Text">The span of UTF-8 characters to parse.</param>
        /// <param name="result">
        ///     When this method returns, contains the Version equivalent of the number that is contained in <paramref name="utf8Text" />, if the conversion succeeded.
        ///     If <paramref name="utf8Text" /> is empty, or if the conversion fails, result is null when the method returns.
        /// </param>
        /// <returns>true if the <paramref name="utf8Text" /> parameter was converted successfully; otherwise, false.</returns>
        public static bool TryParse(ReadOnlySpan<byte> utf8Text, [NotNullWhen(true)] out Version? result)
        {
            result = ParseVersion(utf8Text, throwOnFailure: false);
            return result is not null;
        }
 
        /// <inheritdoc cref="IUtf8SpanParsable{TSelf}.TryParse(ReadOnlySpan{byte}, IFormatProvider?, out TSelf)"/>
        static bool IUtf8SpanParsable<Version>.TryParse(ReadOnlySpan<byte> utf8Text, IFormatProvider? provider, [NotNullWhen(true)] out Version? result)
        {
            result = ParseVersion(utf8Text, throwOnFailure: false);
            return result is not null;
        }
 
        private static Version? ParseVersion<TChar>(ReadOnlySpan<TChar> input, bool throwOnFailure)
            where TChar : unmanaged, IUtfChar<TChar>
        {
            // Find the separator between major and minor.  It must exist.
            int majorEnd = input.IndexOf(TChar.CastFrom('.'));
            if (majorEnd < 0)
            {
                if (throwOnFailure) throw new ArgumentException(SR.Arg_VersionString, nameof(input));
                return null;
            }
 
            // Find the ends of the optional minor and build portions.
            // We musn't have any separators after build.
            int buildEnd = -1;
            int minorEnd = input.Slice(majorEnd + 1).IndexOf(TChar.CastFrom('.'));
            if (minorEnd >= 0)
            {
                minorEnd += (majorEnd + 1);
                buildEnd = input.Slice(minorEnd + 1).IndexOf(TChar.CastFrom('.'));
                if (buildEnd >= 0)
                {
                    buildEnd += (minorEnd + 1);
                    if (input.Slice(buildEnd + 1).Contains(TChar.CastFrom('.')))
                    {
                        if (throwOnFailure) throw new ArgumentException(SR.Arg_VersionString, nameof(input));
                        return null;
                    }
                }
            }
 
            int minor, build, revision;
 
            // Parse the major version
            if (!TryParseComponent(input.Slice(0, majorEnd), nameof(input), throwOnFailure, input, out int major))
            {
                return null;
            }
 
            if (minorEnd != -1)
            {
                // If there's more than a major and minor, parse the minor, too.
                if (!TryParseComponent(input.Slice(majorEnd + 1, minorEnd - majorEnd - 1), nameof(input), throwOnFailure, input, out minor))
                {
                    return null;
                }
 
                if (buildEnd != -1)
                {
                    // major.minor.build.revision
                    return
                        TryParseComponent(input.Slice(minorEnd + 1, buildEnd - minorEnd - 1), nameof(build), throwOnFailure, input, out build) &&
                        TryParseComponent(input.Slice(buildEnd + 1), nameof(revision), throwOnFailure, input, out revision) ?
                            new Version(major, minor, build, revision) :
                            null;
                }
                else
                {
                    // major.minor.build
                    return TryParseComponent(input.Slice(minorEnd + 1), nameof(build), throwOnFailure, input, out build) ?
                        new Version(major, minor, build) :
                        null;
                }
            }
            else
            {
                // major.minor
                return TryParseComponent(input.Slice(majorEnd + 1), nameof(input), throwOnFailure, input, out minor) ?
                    new Version(major, minor) :
                    null;
            }
        }
 
        private static bool TryParseComponent<TChar>(ReadOnlySpan<TChar> component, string componentName, bool throwOnFailure, ReadOnlySpan<TChar> originalInput, out int parsedComponent)
            where TChar : unmanaged, IUtfChar<TChar>
        {
            Number.ParsingStatus parseStatus = Number.TryParseBinaryIntegerStyle(component, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out parsedComponent);
 
            if (parseStatus == Number.ParsingStatus.OK && parsedComponent >= 0)
            {
                return true;
            }
 
            if (throwOnFailure)
            {
                ThrowFailure(parseStatus, parsedComponent, componentName, originalInput);
            }
 
            return false;
 
            [DoesNotReturn]
            static void ThrowFailure(Number.ParsingStatus parseStatus, int parsedComponent, string componentName, ReadOnlySpan<TChar> originalInput)
            {
                ArgumentOutOfRangeException.ThrowIfNegative(parsedComponent, componentName);
 
                if (parseStatus == Number.ParsingStatus.Overflow)
                {
                    Number.ThrowOverflowException<int>();
                }
 
                string inputString = typeof(TChar) == typeof(char) ?
                    Unsafe.BitCast<ReadOnlySpan<TChar>, ReadOnlySpan<char>>(originalInput).ToString() :
                    Encoding.UTF8.GetString(Unsafe.BitCast<ReadOnlySpan<TChar>, ReadOnlySpan<byte>>(originalInput));
 
                throw new FormatException(SR.Format(SR.Format_InvalidStringWithValue, inputString));
            }
        }
 
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static bool operator ==(Version? v1, Version? v2)
        {
            // Test "right" first to allow branch elimination when inlined for null checks (== null)
            // so it can become a simple test
            if (v2 is null)
            {
                return v1 is null;
            }
 
            // Quick reference equality test prior to calling the virtual Equality
            return ReferenceEquals(v2, v1) || v2.Equals(v1);
        }
 
        public static bool operator !=(Version? v1, Version? v2) => !(v1 == v2);
 
        public static bool operator <(Version? v1, Version? v2)
        {
            if (v1 is null)
            {
                return v2 is not null;
            }
 
            return v1.CompareTo(v2) < 0;
        }
 
        public static bool operator <=(Version? v1, Version? v2)
        {
            if (v1 is null)
            {
                return true;
            }
 
            return v1.CompareTo(v2) <= 0;
        }
 
        public static bool operator >(Version? v1, Version? v2) => v2 < v1;
 
        public static bool operator >=(Version? v1, Version? v2) => v2 <= v1;
    }
}