diff --git a/CHANGELOG.md b/CHANGELOG.md index ac2a80a..2d02d17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ All notable changes to **ValueStringBuilder** will be documented in this file. T ### 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. - `Append`/`AppendLine` with an interpolated string could return the rented array to the `ArrayPool` twice when an interpolation hole threw after the buffer grew. Reported by [@dhhoang](https://github.com/dhhoang) in #324 ## [3.7.1] - 2026-09-25 diff --git a/docs/site/articles/exceptions_and_edge_cases.md b/docs/site/articles/exceptions_and_edge_cases.md index 86ce0ec..c3e2850 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 2de5915..5b72ba6 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 3ab1a90..2f17f53 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() {