File: FXVersion.cs
Web Access
Project: ..\..\..\src\Cli\dotnet\dotnet.csproj (dotnet)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
namespace Microsoft.DotNet.MSBuildSdkResolver;
 
// Note: This is SemVer 2.0.0 https://semver.org/spec/v2.0.0.html
// See the original version of this code here: https://github.com/dotnet/core-setup/blob/master/src/corehost/cli/fxr/fx_ver.cpp
internal sealed class FXVersion(int major, int minor, int patch, string pre = "", string build = "")
{
    public int Major { get; } = major;
    public int Minor { get; } = minor;
    public int Patch { get; } = patch;
    public string Pre { get; } = pre;
    public string Build { get; } = build;
 
    private static string GetId(string ids, int idStart)
    {
        int next = ids.IndexOf('.', idStart);
 
        return next == -1 ? ids.Substring(idStart) : ids.Substring(idStart, next - idStart);
    }
 
    public static int Compare(FXVersion s1, FXVersion s2)
    {
        // compare(u.v.w-p+b, x.y.z-q+c)
        if (s1.Major != s2.Major)
        {
            return s1.Major > s2.Major ? 1 : -1;
        }
 
        if (s1.Minor != s2.Minor)
        {
            return s1.Minor > s2.Minor ? 1 : -1;
        }
 
        if (s1.Patch != s2.Patch)
        {
            return s1.Patch > s2.Patch ? 1 : -1;
        }
 
        if (string.IsNullOrEmpty(s1.Pre) || string.IsNullOrEmpty(s2.Pre))
        {
            // Empty (release) is higher precedence than prerelease
            return string.IsNullOrEmpty(s1.Pre) ? (string.IsNullOrEmpty(s2.Pre) ? 0 : 1) : -1;
        }
 
        // Both are non-empty (may be equal)
 
        // First character of pre is '-' when it is not empty
 
        // First identifier starts at position 1
        int idStart = 1;
        for (int i = idStart; true; ++i)
        {
            // C# strings are not null terminated. Pretend to make code similar to fx_ver.cpp
            char s1char = (s1.Pre.Length == i) ? '\0' : s1.Pre[i];
            char s2char = (s2.Pre.Length == i) ? '\0' : s2.Pre[i];
            if (s1char != s2char)
            {
                // Found first character with a difference
                if (s1char == '\0' && s2char == '.')
                {
                    // identifiers both complete, b has an additional identifier
                    return -1;
                }
 
                if (s2char == '\0' && s1char == '.')
                {
                    // identifiers both complete, a has an additional identifier
                    return 1;
                }
 
                // identifiers must not be empty
                string id1 = GetId(s1.Pre, idStart);
                string id2 = GetId(s2.Pre, idStart);
 
                int id1num;
                bool id1IsNum = int.TryParse(id1, out id1num);
                int id2num;
                bool id2IsNum = int.TryParse(id2, out id2num);
 
                if (id1IsNum && id2IsNum)
                {
                    // Numeric comparison
                    return (id1num > id2num) ? 1 : -1;
                }
                else if (id1IsNum || id2IsNum)
                {
                    // Mixed compare.  Spec: Number < Text
                    return id2IsNum ? 1 : -1;
                }
                // Ascii compare
                // Since we are using only ascii characters, unicode ordinal sort == ascii sort
                return (s1char > s2char) ? 1 : -1;
            }
            else
            {
                // s1char == s2char
                if (s1char == '\0')
                {
                    break;
                }
                if (s1char == '.')
                {
                    idStart = i + 1;
                }
            }
        }
        return 0;
    }
 
    private static bool ValidIdentifierCharSet(string id)
    {
        // ids must be of the set [0-9a-zA-Z-]
 
        // ASCII and Unicode ordering
        for (int i = 0; i < id.Length; ++i)
        {
            if (id[i] >= 'A')
            {
                if ((id[i] > 'Z' && id[i] < 'a') || id[i] > 'z')
                {
                    return false;
                }
            }
            else
            {
                if ((id[i] < '0' && id[i] != '-') || id[i] > '9')
                {
                    return false;
                }
            }
        }
        return true;
    }
 
    private static bool ValidIdentifier(string id, bool buildMeta)
    {
        if (string.IsNullOrEmpty(id))
        {
            // Identifier must not be empty
            return false;
        }
 
        if (!ValidIdentifierCharSet(id))
        {
            // ids must be of the set [0-9a-zA-Z-]
            return false;
        }
 
        if (!buildMeta && id[0] == '0' && id.Length > 1 && int.TryParse(id, out _))
        {
            // numeric identifiers must not be padded with 0s
            return false;
        }
        return true;
    }
 
    private static bool ValidIdentifiers(string ids)
    {
        if (string.IsNullOrEmpty(ids))
        {
            return true;
        }
 
        bool prerelease = ids[0] == '-';
        bool buildMeta = ids[0] == '+';
 
        if (!(prerelease || buildMeta))
        {
            // ids must start with '-' or '+' for prerelease & build respectively
            return false;
        }
 
        int idStart = 1;
        int nextId;
        while ((nextId = ids.IndexOf('.', idStart)) != -1)
        {
            if (!ValidIdentifier(ids.Substring(idStart, nextId - idStart), buildMeta))
            {
                return false;
            }
            idStart = nextId + 1;
        }
 
        if (!ValidIdentifier(ids.Substring(idStart), buildMeta))
        {
            return false;
        }
 
        return true;
    }
 
    private static int IndexOfNonNumeric(string s, int startIndex)
    {
        for (int i = startIndex; i < s.Length; ++i)
        {
            if ((s[i] < '0') || (s[i] > '9'))
            {
                return i;
            }
        }
        return -1;
    }
 
    public static bool TryParse(string? fxVersionString, out FXVersion? FXVersion)
    {
        FXVersion = null;
        if (string.IsNullOrEmpty(fxVersionString) || fxVersionString == null)
        {
            return false;
        }
 
        int majorSeparator = fxVersionString.IndexOf(".", StringComparison.Ordinal);
        if (majorSeparator == -1)
        {
            return false;
        }
 
        int major;
        if (!int.TryParse(fxVersionString.Substring(0, majorSeparator), out major))
        {
            return false;
        }
        if (majorSeparator > 1 && fxVersionString[0] == '0')
        {
            // if leading character is 0, and strlen > 1
            // then the numeric substring has leading zeroes which is prohibited by the specification.
            return false;
        }
 
        int minorStart = majorSeparator + 1;
        int minorSeparator = fxVersionString.IndexOf(".", minorStart, StringComparison.Ordinal);
        if (minorSeparator == -1)
        {
            return false;
        }
 
        int minor;
        if (!int.TryParse(fxVersionString.Substring(minorStart, minorSeparator - minorStart), out minor))
        {
            return false;
        }
        if (minorSeparator - minorStart > 1 && fxVersionString[minorStart] == '0')
        {
            // if leading character is 0, and strlen > 1
            // then the numeric substring has leading zeroes which is prohibited by the specification.
            return false;
        }
 
        int patchStart = minorSeparator + 1;
        int patchSeparator = IndexOfNonNumeric(fxVersionString, patchStart);
 
        int patch;
        if (patchSeparator == -1)
        {
            if (!int.TryParse(fxVersionString.Substring(patchStart), out patch))
            {
                return false;
            }
            if (patchStart + 1 < fxVersionString.Length && fxVersionString[patchStart] == '0')
            {
                // if leading character is 0, and strlen != 1
                // then the numeric substring has leading zeroes which is prohibited by the specification.
                return false;
            }
 
            FXVersion = new FXVersion(major, minor, patch);
            return true;
        }
 
        if (!int.TryParse(fxVersionString.Substring(patchStart, patchSeparator - patchStart), out patch))
        {
            return false;
        }
        if (patchSeparator - patchStart > 1 && fxVersionString[patchStart] == '0')
        {
            return false;
        }
 
        int preStart = patchSeparator;
        int preSeparator = fxVersionString.IndexOf("+", preStart, StringComparison.Ordinal);
 
        string pre = (preSeparator == -1) ? fxVersionString.Substring(preStart) : fxVersionString.Substring(preStart, preSeparator - preStart);
 
        if (!ValidIdentifiers(pre))
        {
            return false;
        }
 
        string build = "";
        if (preSeparator != -1)
        {
            build = fxVersionString.Substring(preSeparator);
            if (!ValidIdentifiers(build))
            {
                return false;
            }
        }
 
        FXVersion = new FXVersion(major, minor, patch, pre, build);
 
        return true;
    }
 
    public override string ToString()
        => (!string.IsNullOrEmpty(Pre), !string.IsNullOrEmpty(Build)) switch
        {
            (false, false) => $"{Major}.{Minor}.{Patch}",
            (true, false) => $"{Major}.{Minor}.{Patch}{Pre}",
            (false, true) => $"{Major}.{Minor}.{Patch}{Build}",
            (true, true) => $"{Major}.{Minor}.{Patch}{Pre}{Build}",
        };
}