From f5863a0621729172c8223eb04f45b4c99371231f Mon Sep 17 00:00:00 2001 From: dhhoang Date: Sat, 3 Oct 2026 20:25:25 +0700 Subject: [PATCH 1/2] fix issues where asspan() and indexer might return out-of-range data --- .../articles/exceptions_and_edge_cases.md | 2 +- .../ValueStringBuilder.cs | 9 +---- .../ValueStringBuilderTests.cs | 38 +++++++++++++++++++ 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/docs/site/articles/exceptions_and_edge_cases.md b/docs/site/articles/exceptions_and_edge_cases.md index 86ce0ec7..c3e2850d 100644 --- a/docs/site/articles/exceptions_and_edge_cases.md +++ b/docs/site/articles/exceptions_and_edge_cases.md @@ -12,7 +12,7 @@ uid: exceptions_and_edge_cases | `Insert(int, ...)` overloads | `ArgumentOutOfRangeException` | `index` is negative, or greater than `Length`. | | `Remove(int startIndex, int length)` | `ArgumentOutOfRangeException` | `length` is negative, `startIndex` is negative, or `startIndex + length` is greater than `Length`. | | `Append(char value, int repeatCount)` | `ArgumentOutOfRangeException` | `repeatCount` is negative. | -| `AsSpan(int startIndex, int length)` / `ToString(int startIndex, int length)` | `ArgumentOutOfRangeException` | `length` is greater than `Length`. | +| `AsSpan(int startIndex, int length)` / `ToString(int startIndex, int length)` | `ArgumentOutOfRangeException` | `startIndex` or `length` is negative, or `startIndex + length` is greater than `Length`. | | `AsSpan(Range)` / `ToString(Range)` | `ArgumentOutOfRangeException` | The resolved range falls outside `0..Length`. | | `AppendFormat` | `FormatException` | A placeholder's argument index is not a valid non-negative integer within range of the supplied arguments, or a custom format specifier (e.g. `{0:00}`) is used - see [`AppendFormat` is intentionally limited](xref:best_practices#appendformat-is-intentionally-limited). | | `Insert` (the `ISpanFormattable` overload) | `InvalidOperationException` | `value.TryFormat` does not fit into the temporary buffer sized by `bufferSize` (default 36 characters) - increase `bufferSize` for large custom `ISpanFormattable` types. | diff --git a/src/LinkDotNet.StringBuilder/ValueStringBuilder.cs b/src/LinkDotNet.StringBuilder/ValueStringBuilder.cs index 2de59155..5b72ba66 100644 --- a/src/LinkDotNet.StringBuilder/ValueStringBuilder.cs +++ b/src/LinkDotNet.StringBuilder/ValueStringBuilder.cs @@ -117,7 +117,7 @@ public readonly bool IsEmpty public readonly ref char this[int index] { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => ref buffer[index]; + get => ref buffer[..bufferPosition][index]; } /// @@ -192,12 +192,7 @@ public readonly ref char this[int index] /// The length of the substring. /// The filled array as . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly ReadOnlySpan AsSpan(int startIndex, int length) - { - ArgumentOutOfRangeException.ThrowIfGreaterThan(length, bufferPosition); - - return buffer.Slice(startIndex, length); - } + public readonly ReadOnlySpan AsSpan(int startIndex, int length) => buffer[..bufferPosition].Slice(startIndex, length); /// /// Returns the string as an . diff --git a/tests/LinkDotNet.StringBuilder.UnitTests/ValueStringBuilderTests.cs b/tests/LinkDotNet.StringBuilder.UnitTests/ValueStringBuilderTests.cs index 3ab1a904..2f17f532 100644 --- a/tests/LinkDotNet.StringBuilder.UnitTests/ValueStringBuilderTests.cs +++ b/tests/LinkDotNet.StringBuilder.UnitTests/ValueStringBuilderTests.cs @@ -21,6 +21,25 @@ public void ShouldThrowIndexOutOfRangeWhenStringShorterThanIndex() Assert.False(true); } + [Fact] + public void ShouldThrowIndexOutOfRangeWhenIndexIsBetweenLengthAndCapacity() + { + using var stringBuilder = new ValueStringBuilder(); + stringBuilder.Append("Hello"); + + try + { + _ = stringBuilder[5]; + } + catch (IndexOutOfRangeException) + { + Assert.True(true); + return; + } + + Assert.Fail(); + } + [Fact] public void ShouldTryToCopySpan() { @@ -418,6 +437,25 @@ public void ReturnSubstring() result.ShouldBe("ell"); } + [Fact] + public void ShouldThrowWhenSubstringEndsAfterLength() + { + using var stringBuilder = new ValueStringBuilder(); + stringBuilder.Append("Hello"); + + try + { + stringBuilder.ToString(3, 5); + } + catch (ArgumentOutOfRangeException) + { + Assert.True(true); + return; + } + + Assert.Fail(); + } + [Fact] public void ImplicitCastFromStringToValueStringBuilder() { From 6d79065ddfdb6aab16744a7d7f65a0db2c4c794a Mon Sep 17 00:00:00 2001 From: dhhoang Date: Sun, 4 Oct 2026 06:50:45 +0700 Subject: [PATCH 2/2] Update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78e68cea..c4803498 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ All notable changes to **ValueStringBuilder** will be documented in this file. T ## [Unreleased] +### Fixed + +- `AsSpan(int startIndex, int length)` and `ToString(int startIndex, int length)` throw an `ArgumentOutOfRangeException` when the requested range ends after `Length`, instead of returning characters from the unused part of the buffer. +- The indexer throws an `IndexOutOfRangeException` for an index at or after `Length`, instead of giving access to the unused part of the buffer. + ## [3.7.1] - 2026-09-25 ### Added