fix: avoid infinite recursion when formatting ranges as short npm
TryFormat for the "ns" format returned false for any set that can
not be expressed as a caret/tilde range. The public ToString
routes through DefaultInterpolatedStringHandler, whose buffer-full
fallback re-enters ToString, causing an infinite loop / stack overflow.
For "ns", fall back to the full comparator ("n") form when the
compact form cannot represent the set, so the call always succeeds
for a representable range. A genuine buffer overflow still returns
false as before.
Add failing-then-passing tests.
This commit is contained in:
parent
2ea7866e3b
commit
ae2163188f
3 changed files with 34 additions and 1 deletions
|
|
@ -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.
|
- Reject invalid SemVer identifiers when parsing versions.
|
||||||
- Range exact and non-equal comparators ignore build metadata when matching 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
|
### Removed
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -257,6 +257,21 @@ internal sealed class SemanticVersionRangeTests
|
||||||
await Assert.That(range.Contains(SemanticVersion.Parse("1.2.4"))).IsFalse();
|
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]
|
[Test]
|
||||||
public async Task I_fail_formatting_when_the_tentative_short_form_overflows()
|
public async Task I_fail_formatting_when_the_tentative_short_form_overflows()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -85,9 +85,26 @@ public readonly partial record struct SemanticVersionRange : ISpanFormattable
|
||||||
|
|
||||||
if (format is "ns")
|
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]))
|
if (!TryFormatSimpleNpm(ref buf, sets[i]))
|
||||||
{
|
{
|
||||||
return false;
|
if (buf.Written == writtenBefore)
|
||||||
|
{
|
||||||
|
if (!TryFormatNormalNpm(ref buf, sets[i]))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue