using System.Diagnostics.CodeAnalysis; namespace StardewModdingAPI.Toolkit.Framework { /// Reads strings into a semantic version. internal static class SemanticVersionReader { /********* ** Public methods *********/ /// Parse a semantic version string. /// The version string to parse. /// Whether to recognize non-standard semver extensions. /// The major version incremented for major API changes. /// The minor version incremented for backwards-compatible changes. /// The patch version for backwards-compatible fixes. /// The platform-specific version (if applicable). /// An optional prerelease tag. /// Optional build metadata. This is ignored when determining version precedence. /// Returns whether the version was successfully parsed. 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 *********/ /// Try to parse the next characters in a queue as a numeric part. /// The raw characters to parse. /// The index of the next character to read. /// The parsed part. 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; } /// Try to parse a literal character. /// The raw characters to parse. /// The index of the next character to read. /// The expected character. private static bool TryParseLiteral(char[] raw, ref int index, char ch) { if (index >= raw.Length || raw[index] != ch) return false; index++; return true; } /// Try to parse a tag. /// The raw characters to parse. /// The index of the next character to read. /// The parsed tag. 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; } } }