diff options
Diffstat (limited to 'src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs')
-rw-r--r-- | src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs | 126 |
1 files changed, 126 insertions, 0 deletions
diff --git a/src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs b/src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs new file mode 100644 index 00000000..489e1c4d --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs @@ -0,0 +1,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; + } + } +} |