summaryrefslogtreecommitdiff
path: root/src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs
blob: 939be7715966cfa6e59166c0bec40c865feb77e2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
using System.Diagnostics.CodeAnalysis;

namespace StardewModdingAPI.Toolkit.Framework
{
    /// <summary>Reads strings into a semantic version.</summary>
    internal static class SemanticVersionReader
    {
        /*********
        ** Public methods
        *********/
        /// <summary>Parse a semantic version string.</summary>
        /// <param name="versionStr">The version string to parse.</param>
        /// <param name="allowNonStandard">Whether to recognize non-standard semver extensions.</param>
        /// <param name="major">The major version incremented for major API changes.</param>
        /// <param name="minor">The minor version incremented for backwards-compatible changes.</param>
        /// <param name="patch">The patch version for backwards-compatible fixes.</param>
        /// <param name="platformRelease">The platform-specific version (if applicable).</param>
        /// <param name="prereleaseTag">An optional prerelease tag.</param>
        /// <param name="buildMetadata">Optional build metadata. This is ignored when determining version precedence.</param>
        /// <returns>Returns whether the version was successfully parsed.</returns>
        public static bool TryParse(string? versionStr, bool allowNonStandard, out int major, out int minor, out int patch, out int platformRelease, out string? prereleaseTag, out string? buildMetadata)
        {
            // init
            major = 0;
            minor = 0;
            patch = 0;
            platformRelease = 0;
            prereleaseTag = null;
            buildMetadata = null;

            // normalize
            versionStr = versionStr?.Trim();
            if (string.IsNullOrWhiteSpace(versionStr))
                return false;
            char[] raw = versionStr.ToCharArray();

            // read major/minor version
            int i = 0;
            if (!TryParseVersionPart(raw, ref i, out major) || !TryParseLiteral(raw, ref i, '.') || !TryParseVersionPart(raw, ref i, out minor))
                return false;

            // read optional patch version
            if (TryParseLiteral(raw, ref i, '.') && !TryParseVersionPart(raw, ref i, out patch))
                return false;

            // read optional non-standard platform release version
            if (allowNonStandard && TryParseLiteral(raw, ref i, '.') && !TryParseVersionPart(raw, ref i, out platformRelease))
                return false;

            // read optional prerelease tag
            if (TryParseLiteral(raw, ref i, '-') && !TryParseTag(raw, ref i, out prereleaseTag))
                return false;

            // read optional build tag
            if (TryParseLiteral(raw, ref i, '+') && !TryParseTag(raw, ref i, out buildMetadata))
                return false;

            // validate
            return i == versionStr.Length; // valid if we're at the end
        }


        /*********
        ** Private methods
        *********/
        /// <summary>Try to parse the next characters in a queue as a numeric part.</summary>
        /// <param name="raw">The raw characters to parse.</param>
        /// <param name="index">The index of the next character to read.</param>
        /// <param name="part">The parsed part.</param>
        private static bool TryParseVersionPart(char[] raw, ref int index, out int part)
        {
            part = 0;

            // take digits
            string str = "";
            for (int i = index; i < raw.Length && char.IsDigit(raw[i]); i++)
                str += raw[i];

            // validate
            if (str.Length == 0)
                return false;
            if (str.Length > 1 && str[0] == '0')
                return false; // can't have leading zeros

            // parse
            part = int.Parse(str);
            index += str.Length;
            return true;
        }

        /// <summary>Try to parse a literal character.</summary>
        /// <param name="raw">The raw characters to parse.</param>
        /// <param name="index">The index of the next character to read.</param>
        /// <param name="ch">The expected character.</param>
        private static bool TryParseLiteral(char[] raw, ref int index, char ch)
        {
            if (index >= raw.Length || raw[index] != ch)
                return false;

            index++;
            return true;
        }

        /// <summary>Try to parse a tag.</summary>
        /// <param name="raw">The raw characters to parse.</param>
        /// <param name="index">The index of the next character to read.</param>
        /// <param name="tag">The parsed tag.</param>
        private static bool TryParseTag(char[] raw, ref int index,
#if NET5_0_OR_GREATER
            [NotNullWhen(true)]
#endif
            out string? tag
        )
        {
            // read tag length
            int length = 0;
            for (int i = index; i < raw.Length && (char.IsLetterOrDigit(raw[i]) || raw[i] == '-' || raw[i] == '.'); i++)
                length++;

            // validate
            if (length == 0)
            {
                tag = null;
                return false;
            }

            // parse
            tag = new string(raw, index, length);
            index += length;
            return true;
        }
    }
}