From 60a4f1bbae4590bcaf902705e1efc89ef26e111d Mon Sep 17 00:00:00 2001 From: dylan Date: Wed, 20 May 2026 13:39:20 -0700 Subject: [PATCH 1/4] Reject semver values with leading zeros in local evaluation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per semver 2.0.0 §2, numeric identifiers must not include leading zeros. Values like "1.07.3" are not valid semver and should not match targeting conditions. Both override values and flag values are validated; invalid inputs cause TryParse to return false so the condition does not match. --- src/PostHog/Library/SemanticVersion.cs | 43 ++++++++++----- .../UnitTests/Features/LocalEvaluatorTests.cs | 53 ++++++++++++++++++- .../UnitTests/Library/SemanticVersionTests.cs | 23 ++++++-- 3 files changed, 101 insertions(+), 18 deletions(-) diff --git a/src/PostHog/Library/SemanticVersion.cs b/src/PostHog/Library/SemanticVersion.cs index 7d40088d..249704f3 100644 --- a/src/PostHog/Library/SemanticVersion.cs +++ b/src/PostHog/Library/SemanticVersion.cs @@ -47,6 +47,7 @@ public SemanticVersion(int major, int minor, int patch) /// 5. Default missing components to 0 (e.g., "1.2" → (1, 2, 0), "1" → (1, 0, 0)) /// 6. Ignore extra components beyond the third (e.g., "1.2.3.4" → (1, 2, 3)) /// 7. Return false for truly invalid input (empty string, non-numeric parts, leading dot) + /// 8. Reject components with leading zeros per semver 2.0.0 §2 (e.g., "01.02.03") /// public static bool TryParse(string? value, [NotNullWhen(returnValue: true)] out SemanticVersion? version) { @@ -110,33 +111,49 @@ public static bool TryParse(string? value, [NotNullWhen(returnValue: true)] out var parts = trimmed.Split('.'); // Parse major (required) - if (!int.TryParse(parts[0], out var major)) + if (!TryParseSemverNumeric(parts[0], out var major)) { return false; } // Parse minor (optional, defaults to 0) var minor = 0; - if (parts.Length > 1 && !string.IsNullOrEmpty(parts[1]) && !int.TryParse(parts[1], out minor)) + if (parts.Length > 1 && !string.IsNullOrEmpty(parts[1]) && !TryParseSemverNumeric(parts[1], out minor)) { return false; } // Parse patch (optional, defaults to 0) var patch = 0; - if (parts.Length > 2 && !string.IsNullOrEmpty(parts[2]) && !int.TryParse(parts[2], out patch)) + if (parts.Length > 2 && !string.IsNullOrEmpty(parts[2]) && !TryParseSemverNumeric(parts[2], out patch)) { return false; } - // Reject negative version components - if (major < 0 || minor < 0 || patch < 0) + version = new SemanticVersion(major, minor, patch); + return true; + } + + // Semver 2.0.0 §2: numeric identifiers MUST NOT include leading zeros. + static bool TryParseSemverNumeric(string part, out int value) + { + value = 0; + if (string.IsNullOrEmpty(part)) { return false; } - - version = new SemanticVersion(major, minor, patch); - return true; + if (part.Length > 1 && part[0] == '0') + { + return false; + } + foreach (var c in part) + { + if (!char.IsDigit(c)) + { + return false; + } + } + return int.TryParse(part, out value); } /// @@ -252,7 +269,7 @@ public static bool TryParseWildcard( { // "X" pattern - treat as "X.*" // Bare wildcards like "*" and non-numeric values are invalid - if (!int.TryParse(parts[0], out var major)) + if (!TryParseSemverNumeric(parts[0], out var major)) { return false; } @@ -264,7 +281,7 @@ public static bool TryParseWildcard( else if (parts.Length == 2) { // "X.Y" or "X.*" pattern - if (!int.TryParse(parts[0], out var major)) + if (!TryParseSemverNumeric(parts[0], out var major)) { return false; } @@ -277,7 +294,7 @@ public static bool TryParseWildcard( return true; } - if (!int.TryParse(parts[1], out var minor)) + if (!TryParseSemverNumeric(parts[1], out var minor)) { return false; } @@ -290,12 +307,12 @@ public static bool TryParseWildcard( else if (parts.Length >= 3) { // "X.Y.Z" or "X.Y.*" pattern - if (!int.TryParse(parts[0], out var major)) + if (!TryParseSemverNumeric(parts[0], out var major)) { return false; } - if (!int.TryParse(parts[1], out var minor)) + if (!TryParseSemverNumeric(parts[1], out var minor)) { return false; } diff --git a/tests/UnitTests/Features/LocalEvaluatorTests.cs b/tests/UnitTests/Features/LocalEvaluatorTests.cs index 758efce4..3afeddcf 100644 --- a/tests/UnitTests/Features/LocalEvaluatorTests.cs +++ b/tests/UnitTests/Features/LocalEvaluatorTests.cs @@ -2289,7 +2289,6 @@ public void HandlesSemverWildcardMinorOperator(string overrideValue, ComparisonO [InlineData("1.2.3-alpha", ComparisonOperator.SemverEquals, "1.2.3", true)] // Pre-release stripped [InlineData("1.2.3+build", ComparisonOperator.SemverEquals, "1.2.3", true)] // Build metadata stripped [InlineData(" 1.2.3 ", ComparisonOperator.SemverEquals, "1.2.3", true)] // Whitespace stripped - [InlineData("01.02.03", ComparisonOperator.SemverEquals, "1.2.3", true)] // Leading zeros [InlineData("1.2", ComparisonOperator.SemverEquals, "1.2.0", true)] // Partial version [InlineData("1", ComparisonOperator.SemverEquals, "1.0.0", true)] // Partial version [InlineData("1.2.3.4", ComparisonOperator.SemverEquals, "1.2.3", true)] // Extra parts ignored @@ -2329,6 +2328,14 @@ public void HandlesSpecialVersionFormats(string overrideValue, ComparisonOperato [InlineData(".1.2.3", ComparisonOperator.SemverEquals)] [InlineData("not-a-version", ComparisonOperator.SemverGreaterThan)] [InlineData("", ComparisonOperator.SemverGreaterThan)] + // Leading-zero override values are invalid per semver 2.0.0 §2 + [InlineData("01.02.03", ComparisonOperator.SemverEquals)] + [InlineData("1.07.3", ComparisonOperator.SemverEquals)] + [InlineData("01.02.03", ComparisonOperator.SemverNotEquals)] + [InlineData("1.07.3", ComparisonOperator.SemverGreaterThan)] + [InlineData("01.2.3", ComparisonOperator.SemverGreaterThanOrEquals)] + [InlineData("1.2.03", ComparisonOperator.SemverLessThan)] + [InlineData("1.07.3", ComparisonOperator.SemverLessThanOrEquals)] public void ThrowsInconclusiveMatchExceptionForInvalidOverrideVersion(string overrideValue, ComparisonOperator comparison) { var flags = CreateFlags( @@ -2362,6 +2369,11 @@ public void ThrowsInconclusiveMatchExceptionForInvalidOverrideVersion(string ove [InlineData("")] [InlineData("abc.def.ghi")] [InlineData(".1.2.3")] + // Leading-zero filter values are invalid per semver 2.0.0 §2 + [InlineData("01.02.03")] + [InlineData("1.07.3")] + [InlineData("01.2.3")] + [InlineData("1.2.03")] public void ThrowsInconclusiveMatchExceptionForInvalidFilterVersion(string filterValue) { var flags = CreateFlags( @@ -2389,11 +2401,50 @@ public void ThrowsInconclusiveMatchExceptionForInvalidFilterVersion(string filte personProperties: properties)); } + [Theory] + // Leading-zero flag values are rejected across non-equality operators + [InlineData("1.07.3", ComparisonOperator.SemverGreaterThan)] + [InlineData("01.2.3", ComparisonOperator.SemverGreaterThanOrEquals)] + [InlineData("1.2.03", ComparisonOperator.SemverLessThan)] + [InlineData("01.02.03", ComparisonOperator.SemverLessThanOrEquals)] + [InlineData("1.07.3", ComparisonOperator.SemverTilde)] + [InlineData("01.2.3", ComparisonOperator.SemverCaret)] + public void ThrowsInconclusiveMatchExceptionForLeadingZeroFlagValueAcrossOperators(string filterValue, ComparisonOperator comparison) + { + var flags = CreateFlags( + key: "version", + properties: [ + new PropertyFilter + { + Type = FilterType.Person, + Key = "app_version", + Value = new PropertyFilterValue(filterValue), + Operator = comparison + } + ] + ); + var properties = new Dictionary + { + ["app_version"] = "1.2.3" + }; + var localEvaluator = new LocalEvaluator(flags); + + Assert.Throws(() => + localEvaluator.EvaluateFeatureFlag( + key: "version", + distinctId: "distinct-id", + personProperties: properties)); + } + [Theory] // Invalid wildcard patterns - should throw InconclusiveMatchException [InlineData("*")] [InlineData("1.2.3")] // Not a wildcard pattern [InlineData("abc.*")] + // Leading-zero wildcard patterns are invalid per semver 2.0.0 §2 + [InlineData("01.*")] + [InlineData("1.02.*")] + [InlineData("01.2.*")] public void ThrowsInconclusiveMatchExceptionForInvalidWildcardPattern(string filterValue) { var flags = CreateFlags( diff --git a/tests/UnitTests/Library/SemanticVersionTests.cs b/tests/UnitTests/Library/SemanticVersionTests.cs index 981b678b..ea2caf9e 100644 --- a/tests/UnitTests/Library/SemanticVersionTests.cs +++ b/tests/UnitTests/Library/SemanticVersionTests.cs @@ -107,10 +107,12 @@ public void IgnoresExtraComponents(string input, int expectedMajor, int expected } [Theory] - // Leading zeros are parsed as integers - [InlineData("01.02.03", 1, 2, 3)] - [InlineData("001.002.003", 1, 2, 3)] - public void ParsesLeadingZeros(string input, int expectedMajor, int expectedMinor, int expectedPatch) + // Literal "0" components are valid per semver 2.0.0 + [InlineData("0.0.0", 0, 0, 0)] + [InlineData("0.1.0", 0, 1, 0)] + [InlineData("1.0.0", 1, 0, 0)] + [InlineData("1.2.0", 1, 2, 0)] + public void ParsesLiteralZeroComponents(string input, int expectedMajor, int expectedMinor, int expectedPatch) { var result = SemanticVersion.TryParse(input, out var version); @@ -139,6 +141,13 @@ public void ParsesLeadingZeros(string input, int expectedMajor, int expectedMino [InlineData("1.-2.3")] // Negative minor [InlineData("-1.2.3")] // Negative major [InlineData("1.2.-3")] // Negative patch + [InlineData("01.02.03")] // Leading zeros (all components) + [InlineData("001.002.003")] // Multiple leading zeros + [InlineData("1.07.3")] // Leading zero in minor + [InlineData("1.2.03")] // Leading zero in patch + [InlineData("01.2.3")] // Leading zero in major + [InlineData("00.0.0")] // Leading zero on a zero major + [InlineData("v01.2.3")] // Leading zero with v-prefix public void ReturnsFalseForInvalidInput(string? input) { var result = SemanticVersion.TryParse(input, out var version); @@ -470,6 +479,12 @@ public void ParsesImplicitMinorWildcard(string pattern, string expectedLower, st [InlineData("1.2.3.*")] // Too specific [InlineData(".1.*")] // Leading dot [InlineData("abc.*")] // Non-numeric + [InlineData("01.*")] // Leading zero in major + [InlineData("1.02.*")] // Leading zero in minor + [InlineData("01.2.*")] // Leading zero in major (X.Y.*) + [InlineData("01")] // Leading zero in implicit major + [InlineData("01.2")] // Leading zero in implicit X.Y + [InlineData("1.02")] // Leading zero in implicit minor public void ReturnsFalseForInvalidPatterns(string? pattern) { var result = SemanticVersion.TryParseWildcard(pattern, out var lower, out var upper); From 9e241c635e6ec16170254a0654b474562d66027a Mon Sep 17 00:00:00 2001 From: dylan Date: Wed, 20 May 2026 14:18:51 -0700 Subject: [PATCH 2/4] fix: address review feedback on semver leading-zero rejection - TryParseSemverNumeric: replace the manual char.IsDigit loop with int.TryParse(part, NumberStyles.None, CultureInfo.InvariantCulture, ...), which is single-pass and correctly rejects non-ASCII decimal digits. - Merge ThrowsInconclusiveMatchExceptionForLeadingZeroFlagValueAcrossOperators into ThrowsInconclusiveMatchExceptionForInvalidFilterVersion by adding a ComparisonOperator parameter, matching the existing ThrowsInconclusiveMatchExceptionForInvalidOverrideVersion pattern. --- src/PostHog/Library/SemanticVersion.cs | 17 ++----- .../UnitTests/Features/LocalEvaluatorTests.cs | 49 ++++--------------- 2 files changed, 14 insertions(+), 52 deletions(-) diff --git a/src/PostHog/Library/SemanticVersion.cs b/src/PostHog/Library/SemanticVersion.cs index 249704f3..5a7812df 100644 --- a/src/PostHog/Library/SemanticVersion.cs +++ b/src/PostHog/Library/SemanticVersion.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using System.Globalization; namespace PostHog.Library; @@ -138,22 +139,12 @@ public static bool TryParse(string? value, [NotNullWhen(returnValue: true)] out static bool TryParseSemverNumeric(string part, out int value) { value = 0; - if (string.IsNullOrEmpty(part)) + if (string.IsNullOrEmpty(part) || (part.Length > 1 && part[0] == '0')) { return false; } - if (part.Length > 1 && part[0] == '0') - { - return false; - } - foreach (var c in part) - { - if (!char.IsDigit(c)) - { - return false; - } - } - return int.TryParse(part, out value); + // NumberStyles.None + InvariantCulture rejects signs, whitespace, and non-ASCII digits. + return int.TryParse(part, NumberStyles.None, CultureInfo.InvariantCulture, out value); } /// diff --git a/tests/UnitTests/Features/LocalEvaluatorTests.cs b/tests/UnitTests/Features/LocalEvaluatorTests.cs index 3afeddcf..9129ddb4 100644 --- a/tests/UnitTests/Features/LocalEvaluatorTests.cs +++ b/tests/UnitTests/Features/LocalEvaluatorTests.cs @@ -2365,51 +2365,22 @@ public void ThrowsInconclusiveMatchExceptionForInvalidOverrideVersion(string ove [Theory] // Invalid version in filter value - should throw InconclusiveMatchException - [InlineData("not-a-version")] - [InlineData("")] - [InlineData("abc.def.ghi")] - [InlineData(".1.2.3")] - // Leading-zero filter values are invalid per semver 2.0.0 §2 - [InlineData("01.02.03")] - [InlineData("1.07.3")] - [InlineData("01.2.3")] - [InlineData("1.2.03")] - public void ThrowsInconclusiveMatchExceptionForInvalidFilterVersion(string filterValue) - { - var flags = CreateFlags( - key: "version", - properties: [ - new PropertyFilter - { - Type = FilterType.Person, - Key = "app_version", - Value = new PropertyFilterValue(filterValue), - Operator = ComparisonOperator.SemverEquals - } - ] - ); - var properties = new Dictionary - { - ["app_version"] = "1.2.3" - }; - var localEvaluator = new LocalEvaluator(flags); - - Assert.Throws(() => - localEvaluator.EvaluateFeatureFlag( - key: "version", - distinctId: "distinct-id", - personProperties: properties)); - } - - [Theory] - // Leading-zero flag values are rejected across non-equality operators + [InlineData("not-a-version", ComparisonOperator.SemverEquals)] + [InlineData("", ComparisonOperator.SemverEquals)] + [InlineData("abc.def.ghi", ComparisonOperator.SemverEquals)] + [InlineData(".1.2.3", ComparisonOperator.SemverEquals)] + // Leading-zero filter values are invalid per semver 2.0.0 §2, across all operators + [InlineData("01.02.03", ComparisonOperator.SemverEquals)] + [InlineData("1.07.3", ComparisonOperator.SemverEquals)] + [InlineData("01.2.3", ComparisonOperator.SemverEquals)] + [InlineData("1.2.03", ComparisonOperator.SemverEquals)] [InlineData("1.07.3", ComparisonOperator.SemverGreaterThan)] [InlineData("01.2.3", ComparisonOperator.SemverGreaterThanOrEquals)] [InlineData("1.2.03", ComparisonOperator.SemverLessThan)] [InlineData("01.02.03", ComparisonOperator.SemverLessThanOrEquals)] [InlineData("1.07.3", ComparisonOperator.SemverTilde)] [InlineData("01.2.3", ComparisonOperator.SemverCaret)] - public void ThrowsInconclusiveMatchExceptionForLeadingZeroFlagValueAcrossOperators(string filterValue, ComparisonOperator comparison) + public void ThrowsInconclusiveMatchExceptionForInvalidFilterVersion(string filterValue, ComparisonOperator comparison) { var flags = CreateFlags( key: "version", From a6b0aaed05eb5356702bac51729db76ab5095d19 Mon Sep 17 00:00:00 2001 From: dylan Date: Thu, 21 May 2026 07:45:57 -0700 Subject: [PATCH 3/4] chore: add changeset for semver leading-zero fix --- .changeset/strict-semver-leading-zeros.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/strict-semver-leading-zeros.md diff --git a/.changeset/strict-semver-leading-zeros.md b/.changeset/strict-semver-leading-zeros.md new file mode 100644 index 00000000..d554fa7a --- /dev/null +++ b/.changeset/strict-semver-leading-zeros.md @@ -0,0 +1,5 @@ +--- +"PostHog": patch +--- + +Reject semver values with leading zeros in local flag evaluation. Per semver 2.0.0 §2, numeric identifiers must not include leading zeros — values like `1.07.3` are not valid semver and should not match targeting conditions. Both override values and flag values are now validated; invalid inputs cause `SemanticVersion.TryParse` to return false so the condition does not match. From 36f2a9f1e7b4a59aa35e57db81eeae7551a97ded Mon Sep 17 00:00:00 2001 From: dylan Date: Fri, 22 May 2026 11:01:45 -0700 Subject: [PATCH 4/4] fix: address haacked review feedback on semver leading-zero rejection Split combined empty/leading-zero guard into two ifs, rename helper to TryParseSemverNumericIdentifier to match its doc-comment terminology, and document that NumberStyles.None's sign rejection is what lets us drop the explicit negative-component check. Lock in helper-contract and composition cases the existing rows didn't pin: leading zero combined with pre-release, build metadata, and outer whitespace; embedded whitespace, Arabic-Indic digits, and int overflow inputs; v-prefixed and multi-leading-zero wildcard rejection. Mirror the override-version test by running non-leading-zero malformed inputs under non-equals operators (SemverGreaterThan, SemverTilde, SemverCaret), and swap the duplicate ParsesLiteralZeroComponents("0.0.0") row for "0.0.1". --- src/PostHog/Library/SemanticVersion.cs | 27 +++++++++++-------- .../UnitTests/Features/LocalEvaluatorTests.cs | 6 +++++ .../UnitTests/Library/SemanticVersionTests.cs | 10 ++++++- 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/src/PostHog/Library/SemanticVersion.cs b/src/PostHog/Library/SemanticVersion.cs index 5a7812df..c4517cbd 100644 --- a/src/PostHog/Library/SemanticVersion.cs +++ b/src/PostHog/Library/SemanticVersion.cs @@ -112,21 +112,21 @@ public static bool TryParse(string? value, [NotNullWhen(returnValue: true)] out var parts = trimmed.Split('.'); // Parse major (required) - if (!TryParseSemverNumeric(parts[0], out var major)) + if (!TryParseSemverNumericIdentifier(parts[0], out var major)) { return false; } // Parse minor (optional, defaults to 0) var minor = 0; - if (parts.Length > 1 && !string.IsNullOrEmpty(parts[1]) && !TryParseSemverNumeric(parts[1], out minor)) + if (parts.Length > 1 && !string.IsNullOrEmpty(parts[1]) && !TryParseSemverNumericIdentifier(parts[1], out minor)) { return false; } // Parse patch (optional, defaults to 0) var patch = 0; - if (parts.Length > 2 && !string.IsNullOrEmpty(parts[2]) && !TryParseSemverNumeric(parts[2], out patch)) + if (parts.Length > 2 && !string.IsNullOrEmpty(parts[2]) && !TryParseSemverNumericIdentifier(parts[2], out patch)) { return false; } @@ -135,15 +135,20 @@ public static bool TryParse(string? value, [NotNullWhen(returnValue: true)] out return true; } - // Semver 2.0.0 §2: numeric identifiers MUST NOT include leading zeros. - static bool TryParseSemverNumeric(string part, out int value) + static bool TryParseSemverNumericIdentifier(string part, out int value) { value = 0; - if (string.IsNullOrEmpty(part) || (part.Length > 1 && part[0] == '0')) + if (string.IsNullOrEmpty(part)) + { + return false; + } + // Semver 2.0.0 §2: literal "0" is allowed; "01", "00", "001" are not. + if (part.Length > 1 && part[0] == '0') { return false; } // NumberStyles.None + InvariantCulture rejects signs, whitespace, and non-ASCII digits. + // The sign rejection is what lets TryParse drop the explicit negative-component check. return int.TryParse(part, NumberStyles.None, CultureInfo.InvariantCulture, out value); } @@ -260,7 +265,7 @@ public static bool TryParseWildcard( { // "X" pattern - treat as "X.*" // Bare wildcards like "*" and non-numeric values are invalid - if (!TryParseSemverNumeric(parts[0], out var major)) + if (!TryParseSemverNumericIdentifier(parts[0], out var major)) { return false; } @@ -272,7 +277,7 @@ public static bool TryParseWildcard( else if (parts.Length == 2) { // "X.Y" or "X.*" pattern - if (!TryParseSemverNumeric(parts[0], out var major)) + if (!TryParseSemverNumericIdentifier(parts[0], out var major)) { return false; } @@ -285,7 +290,7 @@ public static bool TryParseWildcard( return true; } - if (!TryParseSemverNumeric(parts[1], out var minor)) + if (!TryParseSemverNumericIdentifier(parts[1], out var minor)) { return false; } @@ -298,12 +303,12 @@ public static bool TryParseWildcard( else if (parts.Length >= 3) { // "X.Y.Z" or "X.Y.*" pattern - if (!TryParseSemverNumeric(parts[0], out var major)) + if (!TryParseSemverNumericIdentifier(parts[0], out var major)) { return false; } - if (!TryParseSemverNumeric(parts[1], out var minor)) + if (!TryParseSemverNumericIdentifier(parts[1], out var minor)) { return false; } diff --git a/tests/UnitTests/Features/LocalEvaluatorTests.cs b/tests/UnitTests/Features/LocalEvaluatorTests.cs index 9129ddb4..eaf5bf5d 100644 --- a/tests/UnitTests/Features/LocalEvaluatorTests.cs +++ b/tests/UnitTests/Features/LocalEvaluatorTests.cs @@ -2369,6 +2369,12 @@ public void ThrowsInconclusiveMatchExceptionForInvalidOverrideVersion(string ove [InlineData("", ComparisonOperator.SemverEquals)] [InlineData("abc.def.ghi", ComparisonOperator.SemverEquals)] [InlineData(".1.2.3", ComparisonOperator.SemverEquals)] + // Mirror the override-version test: non-leading-zero malformed inputs must also fail + // under non-equals operators, not just SemverEquals. + [InlineData("not-a-version", ComparisonOperator.SemverGreaterThan)] + [InlineData("", ComparisonOperator.SemverGreaterThan)] + [InlineData("abc.def.ghi", ComparisonOperator.SemverTilde)] + [InlineData(".1.2.3", ComparisonOperator.SemverCaret)] // Leading-zero filter values are invalid per semver 2.0.0 §2, across all operators [InlineData("01.02.03", ComparisonOperator.SemverEquals)] [InlineData("1.07.3", ComparisonOperator.SemverEquals)] diff --git a/tests/UnitTests/Library/SemanticVersionTests.cs b/tests/UnitTests/Library/SemanticVersionTests.cs index ea2caf9e..9a7e5bf4 100644 --- a/tests/UnitTests/Library/SemanticVersionTests.cs +++ b/tests/UnitTests/Library/SemanticVersionTests.cs @@ -108,7 +108,7 @@ public void IgnoresExtraComponents(string input, int expectedMajor, int expected [Theory] // Literal "0" components are valid per semver 2.0.0 - [InlineData("0.0.0", 0, 0, 0)] + [InlineData("0.0.1", 0, 0, 1)] [InlineData("0.1.0", 0, 1, 0)] [InlineData("1.0.0", 1, 0, 0)] [InlineData("1.2.0", 1, 2, 0)] @@ -148,6 +148,12 @@ public void ParsesLiteralZeroComponents(string input, int expectedMajor, int exp [InlineData("01.2.3")] // Leading zero in major [InlineData("00.0.0")] // Leading zero on a zero major [InlineData("v01.2.3")] // Leading zero with v-prefix + [InlineData("01.2.3-alpha")] // Leading zero + pre-release + [InlineData("01.2.3+build")] // Leading zero + build metadata + [InlineData(" 01.2.3 ")] // Leading zero + outer whitespace + [InlineData("1. 2.3")] // Embedded whitespace in minor (NumberStyles.None rejects) + [InlineData("١.2.3")] // Arabic-Indic digit in major (InvariantCulture rejects) + [InlineData("99999999999999.0.0")] // Overflows int.MaxValue public void ReturnsFalseForInvalidInput(string? input) { var result = SemanticVersion.TryParse(input, out var version); @@ -485,6 +491,8 @@ public void ParsesImplicitMinorWildcard(string pattern, string expectedLower, st [InlineData("01")] // Leading zero in implicit major [InlineData("01.2")] // Leading zero in implicit X.Y [InlineData("1.02")] // Leading zero in implicit minor + [InlineData("v01.*")] // Leading zero with v-prefix + [InlineData("001.*")] // Multiple leading zeros public void ReturnsFalseForInvalidPatterns(string? pattern) { var result = SemanticVersion.TryParseWildcard(pattern, out var lower, out var upper);