diff --git a/CHANGELOG.md b/CHANGELOG.md index 32f4f5c..c4cc6a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Reject invalid SemVer identifiers when parsing versions. - Range exact and non-equal comparators ignore build metadata when matching versions. +- Avoid infinite recursion when formatting ranges in the short npm (`ns`) form. ### Removed diff --git a/src/semver.tests/SemanticVersionRangeTests.cs b/src/semver.tests/SemanticVersionRangeTests.cs index 5bc065f..ea392e1 100644 --- a/src/semver.tests/SemanticVersionRangeTests.cs +++ b/src/semver.tests/SemanticVersionRangeTests.cs @@ -257,6 +257,21 @@ internal sealed class SemanticVersionRangeTests await Assert.That(range.Contains(SemanticVersion.Parse("1.2.4"))).IsFalse(); } + [Test] + [Arguments("[1.2.3,1.4.0)", ">=1.2.3 <1.4.0")] + [Arguments(">1.0.0", ">1.0.0")] + [Arguments("[1.2,1.3]", ">=1.2.0 <=1.3.0")] + [Arguments("(,1.4.0]", "<=1.4.0")] + public async Task I_can_format_ranges_as_short_npm_even_when_not_caret_tilde(string range, string expected) + { + var value = SemanticVersionRange.Parse(range); + var destination = new char[256]; + var success = value.TryFormat(destination, out var charsWritten, "ns", null); + + await Assert.That(success).IsTrue(); + await Assert.That(new string(destination[..charsWritten])).IsEqualTo(expected); + } + [Test] public async Task I_fail_formatting_when_the_tentative_short_form_overflows() { diff --git a/src/semver/SemanticVersionRange.Formatting.cs b/src/semver/SemanticVersionRange.Formatting.cs index 4071601..8b1c570 100644 --- a/src/semver/SemanticVersionRange.Formatting.cs +++ b/src/semver/SemanticVersionRange.Formatting.cs @@ -85,9 +85,26 @@ public readonly partial record struct SemanticVersionRange : ISpanFormattable if (format is "ns") { + var writtenBefore = buf.Written; + + // Try the compact caret/tilde form first. If the set can't be expressed + // that way (TryFormatSimpleNpm returns false without writing anything) fall + // back to the full comparator form so we never report failure for a + // representable range - otherwise the public ToString would recurse + // infinitely via the interpolated-string handler's buffer-overflow fallback. if (!TryFormatSimpleNpm(ref buf, sets[i])) { - return false; + if (buf.Written == writtenBefore) + { + if (!TryFormatNormalNpm(ref buf, sets[i])) + { + return false; + } + } + else + { + return false; + } } } else