Skip to content

NominalWordWrap accepts a NaN width and wraps one character per line instead of throwing ArgumentOutOfRangeException #143

Description

@matt-edmondson

What's wrong

NominalWordWrap (Extensions/StringExtensions.cs:238-253) validates its widths like this:

if (wrapWidth <= 0) throw new ArgumentOutOfRangeException(...);
if (nominalGlyphWidth <= 0) throw new ArgumentOutOfRangeException(...);
...
int maxCharsPerLine = Math.Max(1, (int)Math.Floor(wrapWidth / nominalGlyphWidth));

Every comparison with NaN is false, so a NaN width gets past both checks. (int)Math.Floor(NaN) is 0, and Math.Max(1, 0) turns that into a limit of 1 character per line. The XML docs say ArgumentOutOfRangeException is thrown when either width "is not greater than zero", and NaN is not greater than zero.

Repro (.NET 10, HEAD 0386c21)

  • "hello world foo".NominalWordWrap(float.NaN, 1f) returns 13 lines: h, e, l, l, o, w, o, r, l, d, f, o, o.
  • "hello world foo".NominalWordWrap(10f, float.NaN) gives the same result.

Why it matters

NaN widths are common in UI code, for example a zero-size window divided by zero or an unmeasured font. When one arrives, the text silently turns into a vertical column of single characters instead of failing where the bad value came in.

The same cast has a second problem, found by tracing but not run here. The library also targets net5–net8 and netstandard2.0, where float→int conversion does not saturate. On those targets, wrapWidth = float.PositiveInfinity, or any ratio above int.MaxValue, casts to int.MinValue, Math.Max clamps that to 1, and the text again wraps one character per line. .NET 9+ saturates, so on those runtimes it does not happen.

Suggested fix

if (!(wrapWidth > 0)) throw new ArgumentOutOfRangeException(nameof(wrapWidth), wrapWidth, "...");
if (!(nominalGlyphWidth > 0)) throw new ArgumentOutOfRangeException(nameof(nominalGlyphWidth), nominalGlyphWidth, "...");

double ratio = Math.Floor((double)wrapWidth / nominalGlyphWidth);
int maxCharsPerLine = ratio >= int.MaxValue ? int.MaxValue : Math.Max(1, (int)ratio);

Acceptance criteria

  • Either argument being float.NaN throws ArgumentOutOfRangeException naming that parameter.
  • wrapWidth = float.PositiveInfinity (or a huge ratio) puts each input line on a single output line, on every target framework.
  • Tests cover both cases.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions