summaryrefslogtreecommitdiff
path: root/src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs
blob: 489e1c4daf02dcb081fc3bd272ea4e92ba11abe4 (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
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, 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;
        }
    }
}