diff options
-rw-r--r-- | docs/release-notes.md | 1 | ||||
-rw-r--r-- | src/SMAPI.Tests/Utilities/SemanticVersionTests.cs | 14 | ||||
-rw-r--r-- | src/StardewModdingAPI.Toolkit/SemanticVersion.cs | 27 |
3 files changed, 41 insertions, 1 deletions
diff --git a/docs/release-notes.md b/docs/release-notes.md index a343cff3..152037f8 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -46,6 +46,7 @@ * Fixed some common non-mod build output being included in release zip. * Fixed mods able to intercept other mods' assets via the internal asset keys. * Fixed mods able to indirectly change other mods' data through shared content caches. + * Fixed `SemanticVersion` allowing invalid versions in some cases. * **Breaking changes** (see [migration guide](https://stardewvalleywiki.com/Modding:Migrate_to_Stardew_Valley_1.3)): * Dropped some deprecated APIs. * `LocationEvents` have been rewritten. diff --git a/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs b/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs index feab452a..9091ef09 100644 --- a/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs +++ b/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs @@ -49,6 +49,19 @@ namespace StardewModdingAPI.Tests.Utilities return version.ToString(); } + [Test(Description = "Assert that the constructor throws the expected exception for invalid versions when constructed from the individual numbers.")] + [TestCase(0, 0, 0, null)] + [TestCase(-1, 0, 0, null)] + [TestCase(0, -1, 0, null)] + [TestCase(0, 0, -1, null)] + [TestCase(1, 0, 0, "-tag")] + [TestCase(1, 0, 0, "tag spaces")] + [TestCase(1, 0, 0, "tag~")] + public void Constructor_FromParts_WithInvalidValues(int major, int minor, int patch, string tag) + { + this.AssertAndLogException<FormatException>(() => new SemanticVersion(major, minor, patch, tag)); + } + [Test(Description = "Assert that the constructor sets the expected values for all valid versions when constructed from an assembly version.")] [TestCase(1, 0, 0, ExpectedResult = "1.0")] [TestCase(1, 2, 3, ExpectedResult = "1.2.3")] @@ -79,6 +92,7 @@ namespace StardewModdingAPI.Tests.Utilities [TestCase("1.2.3.apple")] [TestCase("1..2..3")] [TestCase("1.2.3-")] + [TestCase("1.2.3--some-tag")] [TestCase("1.2.3-some-tag...")] [TestCase("1.2.3-some-tag...4")] [TestCase("apple")] diff --git a/src/StardewModdingAPI.Toolkit/SemanticVersion.cs b/src/StardewModdingAPI.Toolkit/SemanticVersion.cs index bd85f990..3008a4d6 100644 --- a/src/StardewModdingAPI.Toolkit/SemanticVersion.cs +++ b/src/StardewModdingAPI.Toolkit/SemanticVersion.cs @@ -10,8 +10,11 @@ namespace StardewModdingAPI.Toolkit /********* ** Properties *********/ + /// <summary>A regex pattern matching a valid prerelease 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>(?>[a-z0-9]+[\-\.]?)+))?"; + internal const string UnboundedVersionPattern = @"(?>(?<major>0|[1-9]\d*))\.(?>(?<minor>0|[1-9]\d*))(?>(?:\.(?<patch>0|[1-9]\d*))?)(?:-(?<prerelease>" + SemanticVersion.TagPattern + "))?"; /// <summary>A regular expression matching a semantic version string.</summary> /// <remarks> @@ -54,6 +57,8 @@ namespace StardewModdingAPI.Toolkit this.Minor = minor; this.Patch = patch; this.Tag = this.GetNormalisedTag(tag); + + this.AssertValid(); } /// <summary>Construct an instance.</summary> @@ -67,6 +72,8 @@ namespace StardewModdingAPI.Toolkit this.Major = version.Major; this.Minor = version.Minor; this.Patch = version.Build; + + this.AssertValid(); } /// <summary>Construct an instance.</summary> @@ -87,6 +94,8 @@ namespace StardewModdingAPI.Toolkit this.Minor = match.Groups["minor"].Success ? int.Parse(match.Groups["minor"].Value) : 0; this.Patch = match.Groups["patch"].Success ? int.Parse(match.Groups["patch"].Value) : 0; this.Tag = match.Groups["prerelease"].Success ? this.GetNormalisedTag(match.Groups["prerelease"].Value) : null; + + this.AssertValid(); } /// <summary>Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version.</summary> @@ -235,5 +244,21 @@ namespace StardewModdingAPI.Toolkit // fallback (this should never happen) return string.Compare(this.ToString(), new SemanticVersion(otherMajor, otherMinor, otherPatch, otherTag).ToString(), StringComparison.InvariantCultureIgnoreCase); } + + /// <summary>Assert that the current version is valid.</summary> + private void AssertValid() + { + if (this.Major < 0 || this.Minor < 0 || this.Patch < 0) + throw new FormatException($"{this} isn't a valid semantic version. The major, minor, and patch numbers can't be negative."); + if (this.Major == 0 && this.Minor == 0 && this.Patch == 0) + throw new FormatException($"{this} isn't a valid semantic version. At least one of the major, minor, and patch numbers must be more than zero."); + if (this.Tag != null) + { + if (this.Tag.Trim() == "") + throw new FormatException($"{this} isn't a valid semantic version. The tag cannot be a blank string (but may be omitted)."); + if (!Regex.IsMatch(this.Tag, $"^{SemanticVersion.TagPattern}$")) + throw new FormatException($"{this} isn't a valid semantic version. The tag is invalid."); + } + } } } |