summaryrefslogtreecommitdiff
path: root/src/SMAPI.Toolkit
diff options
context:
space:
mode:
authorJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2020-01-23 00:31:26 -0500
committerJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2020-01-23 00:31:26 -0500
commitd1935e686c6396519a1ff9b1b429cd55adcf8d11 (patch)
tree3fe7770e4ee13c8fe4d7a25eb041210b42e7b9b6 /src/SMAPI.Toolkit
parent381de5eba9f9822c3483abdf64396cec794e3d03 (diff)
downloadSMAPI-d1935e686c6396519a1ff9b1b429cd55adcf8d11.tar.gz
SMAPI-d1935e686c6396519a1ff9b1b429cd55adcf8d11.tar.bz2
SMAPI-d1935e686c6396519a1ff9b1b429cd55adcf8d11.zip
add full internal support for non-standard four-part versions
Diffstat (limited to 'src/SMAPI.Toolkit')
-rw-r--r--src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs126
-rw-r--r--src/SMAPI.Toolkit/SemanticVersion.cs72
-rw-r--r--src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs2
3 files changed, 173 insertions, 27 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;
+ }
+ }
+}
diff --git a/src/SMAPI.Toolkit/SemanticVersion.cs b/src/SMAPI.Toolkit/SemanticVersion.cs
index 4955dcae..5ead6dc8 100644
--- a/src/SMAPI.Toolkit/SemanticVersion.cs
+++ b/src/SMAPI.Toolkit/SemanticVersion.cs
@@ -1,5 +1,6 @@
using System;
using System.Text.RegularExpressions;
+using StardewModdingAPI.Toolkit.Framework;
namespace StardewModdingAPI.Toolkit
{
@@ -9,6 +10,8 @@ namespace StardewModdingAPI.Toolkit
/// - short-form "x.y" versions are supported (equivalent to "x.y.0");
/// - hyphens are synonymous with dots in prerelease tags and build metadata (like "-unofficial.3-pathoschild");
/// - and "-unofficial" in prerelease tags is always lower-precedence (e.g. "1.0-beta" is newer than "1.0-unofficial").
+ ///
+ /// This optionally also supports four-part versions, a non-standard extension used by Stardew Valley on ported platforms to represent platform-specific patches to a ported version, represented as a fourth number in the version string.
/// </remarks>
public class SemanticVersion : ISemanticVersion
{
@@ -16,14 +19,7 @@ namespace StardewModdingAPI.Toolkit
** Fields
*********/
/// <summary>A regex pattern matching a valid prerelease or build metadata tag.</summary>
- internal const string TagPattern = @"(?>[a-z0-9]+[\-\.]?)+";
-
- /// <summary>A regex pattern matching a version within a larger string.</summary>
- internal const string UnboundedVersionPattern = @"(?>(?<major>0|[1-9]\d*))\.(?>(?<minor>0|[1-9]\d*))(?>(?:\.(?<patch>0|[1-9]\d*))?)(?:-(?<prerelease>" + SemanticVersion.TagPattern + "))?(?:\\+(?<buildmetadata>" + SemanticVersion.TagPattern + "))?";
-
- /// <summary>A regular expression matching a semantic version string.</summary>
- /// <remarks>This pattern is derived from the BNF documentation in the <a href="https://github.com/mojombo/semver">semver repo</a>, with deviations to support the Stardew Valley mod conventions (see remarks on <see cref="SemanticVersion"/>).</remarks>
- internal static readonly Regex Regex = new Regex($@"^{SemanticVersion.UnboundedVersionPattern}$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ExplicitCapture);
+ private const string TagPattern = @"(?>[a-z0-9]+[\-\.]?)+";
/*********
@@ -38,6 +34,9 @@ namespace StardewModdingAPI.Toolkit
/// <summary>The patch version for backwards-compatible bug fixes.</summary>
public int PatchVersion { get; }
+ /// <summary>The platform release. This is a non-standard semver extension used by Stardew Valley on ported platforms to represent platform-specific patches to a ported version, represented as a fourth number in the version string.</summary>
+ public int PlatformRelease { get; }
+
/// <summary>An optional prerelease tag.</summary>
public string PrereleaseTag { get; }
@@ -52,13 +51,15 @@ namespace StardewModdingAPI.Toolkit
/// <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>
- public SemanticVersion(int major, int minor, int patch, string prereleaseTag = null, string buildMetadata = null)
+ public SemanticVersion(int major, int minor, int patch, int platformRelease = 0, string prereleaseTag = null, string buildMetadata = null)
{
this.MajorVersion = major;
this.MinorVersion = minor;
this.PatchVersion = patch;
+ this.PlatformRelease = platformRelease;
this.PrereleaseTag = this.GetNormalizedTag(prereleaseTag);
this.BuildMetadata = this.GetNormalizedTag(buildMetadata);
@@ -82,23 +83,22 @@ namespace StardewModdingAPI.Toolkit
/// <summary>Construct an instance.</summary>
/// <param name="version">The semantic version string.</param>
+ /// <param name="allowNonStandard">Whether to recognize non-standard semver extensions.</param>
/// <exception cref="ArgumentNullException">The <paramref name="version"/> is null.</exception>
/// <exception cref="FormatException">The <paramref name="version"/> is not a valid semantic version.</exception>
- public SemanticVersion(string version)
+ public SemanticVersion(string version, bool allowNonStandard = false)
{
- // parse
if (version == null)
throw new ArgumentNullException(nameof(version), "The input version string can't be null.");
- var match = SemanticVersion.Regex.Match(version.Trim());
- if (!match.Success)
+ if (!SemanticVersionReader.TryParse(version, allowNonStandard, out int major, out int minor, out int patch, out int platformRelease, out string prereleaseTag, out string buildMetadata) || (!allowNonStandard && platformRelease != 0))
throw new FormatException($"The input '{version}' isn't a valid semantic version.");
- // initialize
- this.MajorVersion = int.Parse(match.Groups["major"].Value);
- this.MinorVersion = match.Groups["minor"].Success ? int.Parse(match.Groups["minor"].Value) : 0;
- this.PatchVersion = match.Groups["patch"].Success ? int.Parse(match.Groups["patch"].Value) : 0;
- this.PrereleaseTag = match.Groups["prerelease"].Success ? this.GetNormalizedTag(match.Groups["prerelease"].Value) : null;
- this.BuildMetadata = match.Groups["buildmetadata"].Success ? this.GetNormalizedTag(match.Groups["buildmetadata"].Value) : null;
+ this.MajorVersion = major;
+ this.MinorVersion = minor;
+ this.PatchVersion = patch;
+ this.PlatformRelease = platformRelease;
+ this.PrereleaseTag = prereleaseTag;
+ this.BuildMetadata = buildMetadata;
this.AssertValid();
}
@@ -110,7 +110,7 @@ namespace StardewModdingAPI.Toolkit
{
if (other == null)
throw new ArgumentNullException(nameof(other));
- return this.CompareTo(other.MajorVersion, other.MinorVersion, other.PatchVersion, other.PrereleaseTag);
+ return this.CompareTo(other.MajorVersion, other.MinorVersion, other.PatchVersion, (other as SemanticVersion)?.PlatformRelease ?? 0, other.PrereleaseTag);
}
/// <summary>Indicates whether the current object is equal to another object of the same type.</summary>
@@ -139,7 +139,7 @@ namespace StardewModdingAPI.Toolkit
/// <exception cref="FormatException">The specified version is not a valid semantic version.</exception>
public bool IsOlderThan(string other)
{
- return this.IsOlderThan(new SemanticVersion(other));
+ return this.IsOlderThan(new SemanticVersion(other, allowNonStandard: true));
}
/// <summary>Get whether this version is newer than the specified version.</summary>
@@ -154,7 +154,7 @@ namespace StardewModdingAPI.Toolkit
/// <exception cref="FormatException">The specified version is not a valid semantic version.</exception>
public bool IsNewerThan(string other)
{
- return this.IsNewerThan(new SemanticVersion(other));
+ return this.IsNewerThan(new SemanticVersion(other, allowNonStandard: true));
}
/// <summary>Get whether this version is between two specified versions (inclusively).</summary>
@@ -171,13 +171,15 @@ namespace StardewModdingAPI.Toolkit
/// <exception cref="FormatException">One of the specified versions is not a valid semantic version.</exception>
public bool IsBetween(string min, string max)
{
- return this.IsBetween(new SemanticVersion(min), new SemanticVersion(max));
+ return this.IsBetween(new SemanticVersion(min, allowNonStandard: true), new SemanticVersion(max, allowNonStandard: true));
}
/// <summary>Get a string representation of the version.</summary>
public override string ToString()
{
string version = $"{this.MajorVersion}.{this.MinorVersion}.{this.PatchVersion}";
+ if (this.PlatformRelease != 0)
+ version += $".{this.PlatformRelease}";
if (this.PrereleaseTag != null)
version += $"-{this.PrereleaseTag}";
if (this.BuildMetadata != null)
@@ -185,15 +187,30 @@ namespace StardewModdingAPI.Toolkit
return version;
}
+ /// <summary>Whether the version uses non-standard extensions, like four-part game versions on some platforms.</summary>
+ public bool IsNonStandard()
+ {
+ return this.PlatformRelease != 0;
+ }
+
/// <summary>Parse a version string without throwing an exception if it fails.</summary>
/// <param name="version">The version string.</param>
/// <param name="parsed">The parsed representation.</param>
/// <returns>Returns whether parsing the version succeeded.</returns>
public static bool TryParse(string version, out ISemanticVersion parsed)
{
+ return SemanticVersion.TryParseNonStandard(version, out parsed) && !parsed.IsNonStandard();
+ }
+
+ /// <summary>Parse a version string without throwing an exception if it fails, including support for non-standard extensions like <see cref="IPlatformSpecificVersion"/>.</summary>
+ /// <param name="version">The version string.</param>
+ /// <param name="parsed">The parsed representation.</param>
+ /// <returns>Returns whether parsing the version succeeded.</returns>
+ public static bool TryParseNonStandard(string version, out ISemanticVersion parsed)
+ {
try
{
- parsed = new SemanticVersion(version);
+ parsed = new SemanticVersion(version, true);
return true;
}
catch
@@ -219,8 +236,9 @@ namespace StardewModdingAPI.Toolkit
/// <param name="otherMajor">The major version to compare with this instance.</param>
/// <param name="otherMinor">The minor version to compare with this instance.</param>
/// <param name="otherPatch">The patch version to compare with this instance.</param>
+ /// <param name="otherPlatformRelease">The non-standard platform release to compare with this instance.</param>
/// <param name="otherTag">The prerelease tag to compare with this instance.</param>
- private int CompareTo(int otherMajor, int otherMinor, int otherPatch, string otherTag)
+ private int CompareTo(int otherMajor, int otherMinor, int otherPatch, int otherPlatformRelease, string otherTag)
{
const int same = 0;
const int curNewer = 1;
@@ -233,6 +251,8 @@ namespace StardewModdingAPI.Toolkit
return this.MinorVersion.CompareTo(otherMinor);
if (this.PatchVersion != otherPatch)
return this.PatchVersion.CompareTo(otherPatch);
+ if (this.PlatformRelease != otherPlatformRelease)
+ return this.PlatformRelease.CompareTo(otherPlatformRelease);
if (this.PrereleaseTag == otherTag)
return same;
@@ -274,7 +294,7 @@ namespace StardewModdingAPI.Toolkit
}
// fallback (this should never happen)
- return string.Compare(this.ToString(), new SemanticVersion(otherMajor, otherMinor, otherPatch, otherTag).ToString(), StringComparison.InvariantCultureIgnoreCase);
+ return string.Compare(this.ToString(), new SemanticVersion(otherMajor, otherMinor, otherPatch, otherPlatformRelease, otherTag).ToString(), StringComparison.InvariantCultureIgnoreCase);
}
/// <summary>Assert that the current version is valid.</summary>
diff --git a/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs b/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs
index ece4a72e..e1b9db1d 100644
--- a/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs
+++ b/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs
@@ -68,7 +68,7 @@ namespace StardewModdingAPI.Toolkit.Serialization.Converters
int patch = obj.ValueIgnoreCase<int>(nameof(ISemanticVersion.PatchVersion));
string prereleaseTag = obj.ValueIgnoreCase<string>(nameof(ISemanticVersion.PrereleaseTag));
- return new SemanticVersion(major, minor, patch, prereleaseTag);
+ return new SemanticVersion(major, minor, patch, prereleaseTag: prereleaseTag);
}
/// <summary>Read a JSON string.</summary>