File: NuGetVersionFactory.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.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text;

namespace NuGet.Versioning
{
    public partial class NuGetVersion
    {
        // If dictionary exceeds this size, ParsedNuGetVersionsMapping will be cleared.
        private const int ParsedNuGetVersionsMappingMaxEntries = 500;

        // Cached mappings from string => NuGetVersion. On cache hit, avoids allocations during TryParse.
        private static Dictionary<string, NuGetVersion> ParsedNuGetVersionsMapping = new Dictionary<string, NuGetVersion>(ParsedNuGetVersionsMappingMaxEntries);

        /// <summary>
        /// Creates a NuGetVersion from a string representing the semantic version.
        /// </summary>
        public new static NuGetVersion Parse(string value)
        {
            if (string.IsNullOrEmpty(value))
            {
                throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Argument_Cannot_Be_Null_Or_Empty, value), nameof(value));
            }

            if (!TryParse(value, out NuGetVersion? ver))
            {
                throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Invalidvalue, value), nameof(value));
            }

            return ver;
        }

        /// <summary>
        /// Parses a version string using loose semantic versioning rules that allows 2-4 version components followed
        /// by an optional special version.
        /// </summary>
        public static bool TryParse(string? value, [NotNullWhen(true)] out NuGetVersion? version)
        {
            version = null;

            if (value != null)
            {
                lock (ParsedNuGetVersionsMapping)
                {
                    if (ParsedNuGetVersionsMapping.TryGetValue(value, out version))
                    {
                        return true;
                    }
                }

                Version? systemVersion;

                // trim the value before passing it in since we not strict here
                ParseSections(value.Trim(), out string? versionString, out string[]? releaseLabels, out string? buildMetadata);

                // null indicates the string did not meet the rules
                if (!string.IsNullOrEmpty(versionString))
                {
                    string versionPart = versionString!;

                    if (TryGetNormalizedVersion(versionPart, out systemVersion))
                    {
                        // labels
                        if (releaseLabels != null)
                        {
                            for (int i = 0; i < releaseLabels.Length; i++)
                            {
                                if (!IsValidPart(releaseLabels[i], allowLeadingZeros: false))
                                {
                                    return false;
                                }
                            }
                        }

                        // build metadata
                        if (buildMetadata != null
                            && !IsValid(buildMetadata, allowLeadingZeros: true))
                        {
                            return false;
                        }

                        var originalVersion = value;

                        if (IndexOf(originalVersion, ' ') > -1)
                        {
                            originalVersion =
#if NETCOREAPP2_0_OR_GREATER
                                value.Replace(" ", string.Empty, StringComparison.Ordinal);
#else
                                value.Replace(" ", string.Empty);
#endif
                        }

                        version = new NuGetVersion(version: systemVersion,
                            releaseLabels: releaseLabels,
                            metadata: buildMetadata ?? string.Empty,
                            originalVersion: originalVersion);

                        lock (ParsedNuGetVersionsMapping)
                        {
                            if (ParsedNuGetVersionsMapping.Count >= ParsedNuGetVersionsMappingMaxEntries)
                            {
                                ParsedNuGetVersionsMapping.Clear();
                            }

                            ParsedNuGetVersionsMapping[value] = version;
                        }

                        return true;
                    }
                }
            }

            return false;

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

        private static bool TryGetNormalizedVersion(string str, [NotNullWhen(true)] out Version? version)
        {
            if (string.IsNullOrWhiteSpace(str))
            {
                version = null;
                return false;
            }

            int minor = 0;
            int build = 0;
            int revision = 0;

            // Check for all the possible parts of the version string. If lastParsedPosition is less than the end of
            // the string, the input string was invalid (e.g. "1.2.3.4.5").
            bool success = ParseSection(str, 0, out int lastParsedPosition, out int major) &&
                ParseSection(str, lastParsedPosition, out lastParsedPosition, out minor) &&
                ParseSection(str, lastParsedPosition, out lastParsedPosition, out build) &&
                ParseSection(str, lastParsedPosition, out lastParsedPosition, out revision) &&
                lastParsedPosition == str.Length;

            if (success)
            {
                version = new Version(major, minor, build, revision);
                return true;
            }
            else
            {
                version = null;
                return false;
            }

            // Returns false if an invalid section was found while processing the string.
            static bool ParseSection(string str, int start, out int end, out int versionNumber)
            {
                // Section is empty.
                if (start == str.Length)
                {
                    end = start;
                    versionNumber = 0;
                    return true;
                }

                // Trim off leading whitespace
                for (end = start; end < str.Length; ++end)
                {
                    char currentChar = str[end];
                    if (!char.IsWhiteSpace(currentChar))
                    {
                        if (IsDigit(currentChar))
                        {
                            break;
                        }
                        else
                        {
                            // Found a non-whitespace non-digit character. Invalid string.
                            versionNumber = 0;
                            return false;
                        }
                    }
                }

                bool done = false;
                bool digitFound = false;
                long intermediateVersionNumber = 0;
                // Handle number portion.
                for (; end < str.Length; ++end)
                {
                    // Negative numbers are invalid for version strings so we only need to check for digits.
                    char currentChar = str[end];
                    if (IsDigit(currentChar))
                    {
                        // Parse the values digit by digit and multiplies by 10 to make space for the next digit.
                        // When parsing "123456", this method becomes 1 -> 10 + 2 -> 120 + 3 -> 1230 + 4 -> 12340 + 5 -> 123450 + 6 -> 123456
                        // We subtract off ASCII value of '0' from our current character to get the digit's value
                        // e.g. '3' - '0' == 51 - 48 == 3
                        digitFound = true;
                        intermediateVersionNumber = intermediateVersionNumber * 10 + currentChar - '0';

                        // Check for overflow. We can't get outside the bounds of intermediateVersionNumber, a long, before exceeding int.MaxValue
                        // Intentionally avoid usage of 'checked' statement to avoid exception
                        if (intermediateVersionNumber > int.MaxValue)
                        {
                            versionNumber = 0;
                            return false;
                        }
                    }
                    else if (currentChar == '.')
                    {
                        ++end;
                        // version string ended with '.'
                        if (end == str.Length)
                        {
                            versionNumber = 0;
                            return false;
                        }

                        done = true;
                        break;
                    }
                    else if (char.IsWhiteSpace(currentChar))
                    {
                        break;
                    }
                    else
                    {
                        versionNumber = 0;
                        return false;
                    }
                }

                // We failed to find a number in the section, so the string is invalid.
                if (!digitFound)
                {
                    versionNumber = 0;
                    return false;
                }

                if (end == str.Length)
                {
                    done = true;
                }

                if (!done)
                {
                    // trailing whitespace
                    for (; end < str.Length; ++end)
                    {
                        char currentChar = str[end];
                        if (!char.IsWhiteSpace(currentChar))
                        {
                            if (currentChar == '.')
                            {
                                ++end;
                                // version string ended with '.'
                                if (end == str.Length)
                                {
                                    versionNumber = 0;
                                    return false;
                                }

                                break;
                            }
                            else
                            {
                                versionNumber = 0;
                                return false;
                            }
                        }
                    }
                }

                // Previous checks guarantee returnValue <= int.MaxValue
                versionNumber = (int)intermediateVersionNumber;
                return true;
            }

            static bool IsDigit(char c)
            {
                return c >= '0' && c <= '9';
            }
        }

        /// <summary>
        /// Parses a version string using strict SemVer rules.
        /// </summary>
        public static bool TryParseStrict(string value, [NotNullWhen(true)] out NuGetVersion? version)
        {
            version = null;

            if (TryParse(value, out SemanticVersion? semVer))
            {
                version = new NuGetVersion(semVer.Major, semVer.Minor, semVer.Patch, 0, semVer.ReleaseLabels, semVer.Metadata);
                return true;
            }

            return false;
        }

        /// <summary>
        /// Creates a legacy version string using System.Version
        /// </summary>
        private static string GetLegacyString(Version version, IEnumerable<string>? releaseLabels, string? metadata)
        {
            var sb = new StringBuilder(version.ToString());

            if (releaseLabels != null)
            {
                sb.AppendFormat(CultureInfo.InvariantCulture, "-{0}", string.Join(".", releaseLabels));
            }

            if (!string.IsNullOrEmpty(metadata))
            {
                sb.AppendFormat(CultureInfo.InvariantCulture, "+{0}", metadata);
            }

            return sb.ToString();
        }

        private static IEnumerable<string>? ParseReleaseLabels(string? releaseLabels)
        {
            if (!string.IsNullOrEmpty(releaseLabels))
            {
                return releaseLabels!.Split('.');
            }

            return null;
        }
    }
}