From e8dbfb46725f0848fb9543cab0fd854227ded206 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 15:28:06 +0000 Subject: [PATCH 1/2] fix: reject NaN widths in NominalWordWrap and clamp an unbounded width ratio [patch] A NaN wrap or glyph width failed both `<= 0` checks, and the NaN ratio cast to 0 and was clamped to one character per line. NaN now throws the documented ArgumentOutOfRangeException. The width ratio is now computed in double and clamped before the cast, so an infinite or huge ratio no longer becomes int.MinValue (and so one character per line) on targets before .NET 9, where float-to-int does not saturate. Fixes #143 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VwgZGoivWyqL4pWDsenTPm --- Extensions.Test/StringExtensionsTests.cs | 26 ++++++++++++++++++++++++ Extensions/StringExtensions.cs | 10 ++++++--- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/Extensions.Test/StringExtensionsTests.cs b/Extensions.Test/StringExtensionsTests.cs index fbc7678..f356669 100644 --- a/Extensions.Test/StringExtensionsTests.cs +++ b/Extensions.Test/StringExtensionsTests.cs @@ -470,6 +470,32 @@ public void NominalWordWrapThrowsArgumentOutOfRangeExceptionWhenGlyphWidthNotPos Assert.ThrowsExactly(() => input.NominalWordWrap(100f, 0f).ToList()); } + [TestMethod] + public void NominalWordWrapThrowsArgumentOutOfRangeExceptionWhenWrapWidthIsNaN() + { + string input = "hello world foo"; + ArgumentOutOfRangeException exception = Assert.ThrowsExactly(() => input.NominalWordWrap(float.NaN, 1f).ToList()); + Assert.AreEqual("wrapWidth", exception.ParamName); + } + + [TestMethod] + public void NominalWordWrapThrowsArgumentOutOfRangeExceptionWhenGlyphWidthIsNaN() + { + string input = "hello world foo"; + ArgumentOutOfRangeException exception = Assert.ThrowsExactly(() => input.NominalWordWrap(10f, float.NaN).ToList()); + Assert.AreEqual("nominalGlyphWidth", exception.ParamName); + } + + [TestMethod] + [DataRow(float.PositiveInfinity, 1f)] + [DataRow(float.MaxValue, float.Epsilon)] + public void NominalWordWrapKeepsEachLineWholeWhenWidthRatioIsUnbounded(float wrapWidth, float nominalGlyphWidth) + { + string input = "hello world foo\nbar baz"; + List result = [.. input.NominalWordWrap(wrapWidth, nominalGlyphWidth)]; + CollectionAssert.AreEqual(new List { "hello world foo", "bar baz" }, result); + } + [TestMethod] public void NominalWordWrapBreaksAfterVisibleHyphen() { diff --git a/Extensions/StringExtensions.cs b/Extensions/StringExtensions.cs index f1ea5b2..87212d6 100644 --- a/Extensions/StringExtensions.cs +++ b/Extensions/StringExtensions.cs @@ -235,12 +235,13 @@ public static IEnumerable NominalWordWrap(this string text, float wrapWi { Ensure.NotNull(text); - if (wrapWidth <= 0) + // NaN fails every comparison, so it has to be rejected explicitly. + if (float.IsNaN(wrapWidth) || wrapWidth <= 0) { throw new ArgumentOutOfRangeException(nameof(wrapWidth), wrapWidth, "Wrap width must be greater than zero."); } - if (nominalGlyphWidth <= 0) + if (float.IsNaN(nominalGlyphWidth) || nominalGlyphWidth <= 0) { throw new ArgumentOutOfRangeException(nameof(nominalGlyphWidth), nominalGlyphWidth, "Nominal glyph width must be greater than zero."); } @@ -250,7 +251,10 @@ public static IEnumerable NominalWordWrap(this string text, float wrapWi return []; } - int maxCharsPerLine = Math.Max(1, (int)Math.Floor(wrapWidth / nominalGlyphWidth)); + // Clamp before casting: float-to-int conversion does not saturate before .NET 9, so an infinite or huge ratio + // would otherwise become int.MinValue and wrap one character per line. + double ratio = Math.Floor((double)wrapWidth / nominalGlyphWidth); + int maxCharsPerLine = ratio >= int.MaxValue ? int.MaxValue : Math.Max(1, (int)ratio); return NominalWordWrapIterator(text, maxCharsPerLine); } From ef397d78603ef49157f0621b16f699248be6ee15 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 15:40:54 +0000 Subject: [PATCH 2/2] test: use Assert.AreSequenceEqual in the unbounded-ratio wrap test (MSTEST0068) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VwgZGoivWyqL4pWDsenTPm --- Extensions.Test/StringExtensionsTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Extensions.Test/StringExtensionsTests.cs b/Extensions.Test/StringExtensionsTests.cs index f356669..bc10561 100644 --- a/Extensions.Test/StringExtensionsTests.cs +++ b/Extensions.Test/StringExtensionsTests.cs @@ -493,7 +493,7 @@ public void NominalWordWrapKeepsEachLineWholeWhenWidthRatioIsUnbounded(float wra { string input = "hello world foo\nbar baz"; List result = [.. input.NominalWordWrap(wrapWidth, nominalGlyphWidth)]; - CollectionAssert.AreEqual(new List { "hello world foo", "bar baz" }, result); + Assert.AreSequenceEqual(new List { "hello world foo", "bar baz" }, result); } [TestMethod]