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;
}
}
}