File: FloatRange.cs
Web Access
Project: src\src\nuget-client\src\NuGet.Core\NuGet.Versioning\NuGet.Versioning.csproj (NuGet.Versioning)
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text;
using NuGet.Shared;

namespace NuGet.Versioning
{
    /// <summary>
    /// The floating subset of a version range.
    /// </summary>
    public class FloatRange : IEquatable<FloatRange>
    {
        private readonly NuGetVersion _minVersion;
        private readonly NuGetVersionFloatBehavior _floatBehavior;
        private readonly string? _releasePrefix;

        /// <summary>
        /// Create a floating range.
        /// </summary>
        /// <param name="floatBehavior">Section to float.</param>
        public FloatRange(NuGetVersionFloatBehavior floatBehavior)
            : this(floatBehavior,
                  minVersion: floatBehavior == NuGetVersionFloatBehavior.None ? new NuGetVersion(0, 0, 0) : new NuGetVersion(0, 0, 0, releaseLabel: "0"),
                  releasePrefix: null)
        {
        }

        /// <summary>
        /// Create a floating range.
        /// </summary>
        /// <param name="floatBehavior">Section to float.</param>
        /// <param name="minVersion">Min version of the range.</param>
        public FloatRange(NuGetVersionFloatBehavior floatBehavior, NuGetVersion minVersion)
            : this(floatBehavior, minVersion, null)
        {
        }

        /// <summary>
        /// FloatRange
        /// </summary>
        /// <param name="floatBehavior">Section to float.</param>
        /// <param name="minVersion">Min version of the range.</param>
        /// <param name="releasePrefix">The original release label. Invalid labels are allowed here.</param>
        public FloatRange(NuGetVersionFloatBehavior floatBehavior, NuGetVersion minVersion, string? releasePrefix)
        {
            _floatBehavior = floatBehavior;
            _minVersion = minVersion ?? throw new ArgumentNullException(nameof(minVersion));
            _releasePrefix = releasePrefix;

            if (_releasePrefix == null
                && minVersion != null
                && minVersion.IsPrerelease)
            {
                // use the actual label if one was not given
                _releasePrefix = minVersion.Release;
            }

            if (_floatBehavior == NuGetVersionFloatBehavior.AbsoluteLatest && _releasePrefix is null)
            {
                _releasePrefix = string.Empty;
            }

            if (IncludePrerelease && _releasePrefix == null)
            {
                throw new ArgumentNullException(nameof(releasePrefix));
            }
        }

        /// <summary>
        /// True if a min range exists.
        /// </summary>
        public bool HasMinVersion => _minVersion != null;
        /// <summary>
        /// The minimum version of the float range. This is null for cases such as *
        /// </summary>
        public NuGetVersion MinVersion => _minVersion;

        /// <summary>
        /// Defined float behavior
        /// </summary>
        public NuGetVersionFloatBehavior FloatBehavior => _floatBehavior;

        /// <summary>
        /// The original release label. Invalid labels are allowed here.
        /// </summary>
        public string? OriginalReleasePrefix => _releasePrefix;

        /// <summary>
        /// Indicates if the <see cref=" FloatBehavior"/> includes prerelease versions.
        /// </summary>
        [MemberNotNullWhen(true, nameof(OriginalReleasePrefix))]
        [MemberNotNullWhen(true, nameof(_releasePrefix))]
        public bool IncludePrerelease
            => _floatBehavior switch
            {
                NuGetVersionFloatBehavior.AbsoluteLatest => true,
                NuGetVersionFloatBehavior.Prerelease => true,
                NuGetVersionFloatBehavior.PrereleaseMajor => true,
                NuGetVersionFloatBehavior.PrereleaseMinor => true,
                NuGetVersionFloatBehavior.PrereleasePatch => true,
                NuGetVersionFloatBehavior.PrereleaseRevision => true,
                _ => false
            };

        /// <summary>
        /// True if the given version falls into the floating range.
        /// </summary>
        public bool Satisfies(NuGetVersion version)
        {
            if (version == null)
            {
                throw new ArgumentNullException(nameof(version));
            }

            if (_floatBehavior == NuGetVersionFloatBehavior.AbsoluteLatest)
            {
                return true;
            }

            if (_floatBehavior == NuGetVersionFloatBehavior.Major
                && !version.IsPrerelease)
            {
                return true;
            }

            if (IncludePrerelease)
            {
                // everything beyond this point requires a version
                if (_floatBehavior == NuGetVersionFloatBehavior.PrereleaseRevision)
                {
                    // allow the stable version to match
                    return _minVersion.Major == version.Major
                        && _minVersion.Minor == version.Minor
                        && _minVersion.Patch == version.Patch
                        && ((version.IsPrerelease && version.Release.StartsWith(_releasePrefix, StringComparison.OrdinalIgnoreCase))
                            || !version.IsPrerelease);
                }
                else if (_floatBehavior == NuGetVersionFloatBehavior.PrereleasePatch)
                {
                    // allow the stable version to match
                    return _minVersion.Major == version.Major
                        && _minVersion.Minor == version.Minor
                        && ((version.IsPrerelease && version.Release.StartsWith(_releasePrefix, StringComparison.OrdinalIgnoreCase))
                            || !version.IsPrerelease);
                }
                else if (FloatBehavior == NuGetVersionFloatBehavior.PrereleaseMinor)
                {
                    // allow the stable version to match
                    return _minVersion.Major == version.Major
                        && ((version.IsPrerelease && version.Release.StartsWith(_releasePrefix, StringComparison.OrdinalIgnoreCase))
                            || !version.IsPrerelease);
                }
                else if (FloatBehavior == NuGetVersionFloatBehavior.PrereleaseMajor)
                {
                    // allow the stable version to match
                    return (version.IsPrerelease && version.Release.StartsWith(_releasePrefix, StringComparison.OrdinalIgnoreCase))
                            || !version.IsPrerelease;
                }
                else if (_floatBehavior == NuGetVersionFloatBehavior.Prerelease)
                {
                    // allow the stable version to match
                    return VersionComparer.Version.Equals(_minVersion, version)
                            && ((version.IsPrerelease && version.Release.StartsWith(_releasePrefix, StringComparison.OrdinalIgnoreCase))
                                || !version.IsPrerelease);
                }
            }
            else if (_floatBehavior == NuGetVersionFloatBehavior.Revision)
            {
                return _minVersion.Major == version.Major
                        && _minVersion.Minor == version.Minor
                        && _minVersion.Patch == version.Patch
                        && !version.IsPrerelease;
            }
            else if (_floatBehavior == NuGetVersionFloatBehavior.Patch)
            {
                return _minVersion.Major == version.Major
                        && _minVersion.Minor == version.Minor
                        && !version.IsPrerelease;
            }
            else if (_floatBehavior == NuGetVersionFloatBehavior.Minor)
            {
                return _minVersion.Major == version.Major
                        && !version.IsPrerelease;
            }

            return false;
        }

        /// <summary>
        /// Parse a floating version into a FloatRange
        /// </summary>
        public static FloatRange Parse(string versionString)
        {
            if (versionString == null)
            {
                throw new ArgumentNullException(nameof(versionString));
            }

            if (!TryParse(versionString, out FloatRange? range))
            {
                throw new FormatException(string.Format(CultureInfo.CurrentCulture, Resources.InvalidFloatRangeValue, versionString));
            }

            return range;
        }

        /// <summary>
        /// Parse a floating version into a FloatRange
        /// </summary>
        public static bool TryParse(string versionString, [NotNullWhen(true)] out FloatRange? range)
        {
            range = null;

            if (versionString != null && !string.IsNullOrWhiteSpace(versionString))
            {
                var firstStarPosition = IndexOf(versionString, '*');
                var lastStarPosition = versionString.LastIndexOf('*');
                string? releasePrefix = null;

                if (versionString.Length == 1
                    && firstStarPosition == 0)
                {
                    range = new FloatRange(NuGetVersionFloatBehavior.Major, new NuGetVersion(new Version(0, 0)));
                }
                else if (versionString.Equals("*-*", StringComparison.Ordinal))
                {
                    range = new FloatRange(NuGetVersionFloatBehavior.AbsoluteLatest, new NuGetVersion("0.0.0-0"), releasePrefix: string.Empty);
                }
                else if (firstStarPosition != lastStarPosition && lastStarPosition != -1 && IndexOf(versionString, '+') == -1)
                {
                    var behavior = NuGetVersionFloatBehavior.None;
                    // 2 *s are only allowed in prerelease versions.
                    var dashPosition = IndexOf(versionString, '-');
                    string? actualVersion = null;

                    if (dashPosition != -1 &&
                        lastStarPosition == versionString.Length - 1 && // Last star is at the end of the full string
                        firstStarPosition == (dashPosition - 1) // First star is right before the first dash.
                        )
                    {
                        // Get the stable part.
                        var stablePart = versionString.Substring(0, dashPosition - 1); // Get the part without the *
                        stablePart += "0";
                        var versionParts = CalculateVersionParts(stablePart);
                        switch (versionParts)
                        {
                            case 1:
                                behavior = NuGetVersionFloatBehavior.PrereleaseMajor;
                                break;
                            case 2:
                                behavior = NuGetVersionFloatBehavior.PrereleaseMinor;
                                break;
                            case 3:
                                behavior = NuGetVersionFloatBehavior.PrereleasePatch;
                                break;
                            case 4:
                                behavior = NuGetVersionFloatBehavior.PrereleaseRevision;
                                break;
                            default:
                                break;
                        }

                        var releaseVersion = versionString.Substring(dashPosition + 1);
                        releasePrefix = releaseVersion.Substring(0, releaseVersion.Length - 1);
                        var releasePart = releasePrefix;
                        if (releasePrefix.Length == 0 || releasePrefix.EndsWith(".", StringComparison.Ordinal))
                        {
                            // 1.0.0-* scenario, an empty label is not a valid version.
                            releasePart += "0";
                        }

                        actualVersion = stablePart + "-" + releasePart;
                    }

                    if (NuGetVersion.TryParse(actualVersion, out NuGetVersion? version))
                    {
                        range = new FloatRange(behavior, version, releasePrefix);
                    }
                }
                // A single * can only appear as the last char in the string. 
                // * cannot appear in the metadata section after the +
                else if (lastStarPosition == versionString.Length - 1 && IndexOf(versionString, '+') == -1)
                {
                    var behavior = NuGetVersionFloatBehavior.None;

                    var actualVersion = versionString.Substring(0, versionString.Length - 1);

                    if (IndexOf(versionString, '-') == -1)
                    {
                        // replace the * with a 0
                        actualVersion += "0";

                        var versionParts = CalculateVersionParts(actualVersion);

                        if (versionParts == 2)
                        {
                            behavior = NuGetVersionFloatBehavior.Minor;
                        }
                        else if (versionParts == 3)
                        {
                            behavior = NuGetVersionFloatBehavior.Patch;
                        }
                        else if (versionParts == 4)
                        {
                            behavior = NuGetVersionFloatBehavior.Revision;
                        }
                    }
                    else
                    {
                        behavior = NuGetVersionFloatBehavior.Prerelease;

                        // check for a prefix
                        if (IndexOf(versionString, '-') == versionString.LastIndexOf('-'))
                        {
                            releasePrefix = actualVersion.Substring(versionString.LastIndexOf('-') + 1);

                            // For numeric labels 0 is the lowest. For alpha-numeric - is the lowest.
                            if (releasePrefix.Length == 0 || actualVersion.EndsWith(".", StringComparison.Ordinal))
                            {
                                // 1.0.0-* scenario, an empty label is not a valid version.
                                actualVersion += "0";
                            }
                            else if (actualVersion.EndsWith("-", StringComparison.Ordinal))
                            {
                                // Append a dash to allow floating on the next character.
                                actualVersion += "-";
                            }
                        }
                    }

                    if (NuGetVersion.TryParse(actualVersion, out NuGetVersion? version))
                    {
                        range = new FloatRange(behavior, version, releasePrefix);
                    }
                }
                else
                {
                    // normal version parse
                    if (NuGetVersion.TryParse(versionString, out NuGetVersion? version))
                    {
                        // there is no float range for this version
                        range = new FloatRange(NuGetVersionFloatBehavior.None, version);
                    }
                }
            }

            return range != null;

            int IndexOf(string str, char c)
            {
#if NETCOREAPP2_0_OR_GREATER
                return str.IndexOf(c, StringComparison.Ordinal);
#else
                return str.IndexOf(c);
#endif
            }
        }

        private static int CalculateVersionParts(string line)
        {
            var count = 1;
            if (line != null)
            {
                for (var i = 0; i < line.Length; i++)
                {
                    if (line[i] == '.')
                    {
                        count++;
                    }
                }
            }
            return count;
        }

        /// <summary>
        /// Create a floating version string in the format: 1.0.0-alpha-*
        /// </summary>
        public override string ToString()
        {
            StringBuilder sb = StringBuilderPool.Shared.Rent(256);

            ToString(sb);

            return StringBuilderPool.Shared.ToStringAndReturn(sb);
        }

        /// <summary>
        /// Create a floating version string in the format: 1.0.0-alpha-*
        /// </summary>
        public void ToString(StringBuilder sb)
        {
            if (sb == null)
            {
                throw new ArgumentNullException(nameof(sb));
            }
            switch (_floatBehavior)
            {
                case NuGetVersionFloatBehavior.None:
                    sb.Append(MinVersion.ToNormalizedString());
                    break;
                case NuGetVersionFloatBehavior.Prerelease:
                    sb.AppendFormat(VersionFormatter.Instance, "{0:V}-{1}*", MinVersion, _releasePrefix);
                    break;
                case NuGetVersionFloatBehavior.Revision:
                    sb.AppendFormat(CultureInfo.InvariantCulture, "{0}.{1}.{2}.*", MinVersion.Major, MinVersion.Minor, MinVersion.Patch);
                    break;
                case NuGetVersionFloatBehavior.Patch:
                    sb.AppendFormat(CultureInfo.InvariantCulture, "{0}.{1}.*", MinVersion.Major, MinVersion.Minor);
                    break;
                case NuGetVersionFloatBehavior.Minor:
                    sb.AppendFormat(CultureInfo.InvariantCulture, "{0}.*", MinVersion.Major);
                    break;
                case NuGetVersionFloatBehavior.Major:
                    sb.Append('*');
                    break;
                case NuGetVersionFloatBehavior.PrereleaseRevision:
                    sb.AppendFormat(CultureInfo.InvariantCulture, "{0}.{1}.{2}.*-{3}*", MinVersion.Major, MinVersion.Minor, MinVersion.Patch, _releasePrefix);
                    break;
                case NuGetVersionFloatBehavior.PrereleasePatch:
                    sb.AppendFormat(CultureInfo.InvariantCulture, "{0}.{1}.*-{2}*", MinVersion.Major, MinVersion.Minor, _releasePrefix);
                    break;
                case NuGetVersionFloatBehavior.PrereleaseMinor:
                    sb.AppendFormat(CultureInfo.InvariantCulture, "{0}.*-{1}*", MinVersion.Major, _releasePrefix);
                    break;
                case NuGetVersionFloatBehavior.PrereleaseMajor:
                    sb.AppendFormat(CultureInfo.InvariantCulture, "*-{1}*", MinVersion.Major, _releasePrefix);
                    break;
                case NuGetVersionFloatBehavior.AbsoluteLatest:
                    sb.Append("*-*");
                    break;
                default:
                    break;
            }
        }

        /// <summary>
        /// Equals
        /// </summary>
        public bool Equals(FloatRange? other)
        {
            return FloatBehavior == other?.FloatBehavior
#pragma warning disable CS8604 // Possible null reference argument.
                   // BCL is missing nullable annotations on IComparer<T> before net5.0
                   && VersionComparer.Default.Equals(MinVersion, other?.MinVersion);
#pragma warning restore CS8604 // Possible null reference argument.
        }

        /// <summary>
        /// Override Object.Equals
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public override bool Equals(object? obj)
        {
            return Equals(obj as FloatRange);
        }

        /// <summary>
        /// Hash code
        /// </summary>
        public override int GetHashCode()
        {
            var combiner = new HashCodeCombiner();

            combiner.AddStruct(FloatBehavior);
            combiner.AddObject(MinVersion);

            return combiner.CombinedHash;
        }
    }
}