Skip to content

Commit c4507d7

Browse files
Fix various LastIndexOf bugs when given zero-length target values (#34616)
- string.LastIndexOf(string.Empty) shouldn't perform -1 adjustment - MemoryExtensions.LastIndexOf(ROS<char>, string.Empty) shouldn't return 0 - Tighten up some existing unit tests
1 parent f8d38f4 commit c4507d7

24 files changed

+262
-162
lines changed

src/libraries/Common/tests/Tests/System/StringTests.cs

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3379,7 +3379,6 @@ public static void IndexOfSequenceLengthOneValue_Char()
33793379
Assert.Equal(2, index);
33803380
Assert.Equal(index, s1.IndexOf(s2, StringComparison.Ordinal));
33813381

3382-
// A zero-length value is always "found" at the start of the span.
33833382
ReadOnlySpan<char> span = s1.AsSpan();
33843383
ReadOnlySpan<char> value = s2.AsSpan();
33853384
index = span.IndexOf(value);
@@ -3396,7 +3395,6 @@ public static void IndexOfSequenceLengthOneValueAtVeryEnd_Char()
33963395
Assert.Equal(5, index);
33973396
Assert.Equal(index, s1.IndexOf(s2, StringComparison.Ordinal));
33983397

3399-
// A zero-length value is always "found" at the start of the span.
34003398
ReadOnlySpan<char> span = s1.AsSpan();
34013399
ReadOnlySpan<char> value = s2.AsSpan();
34023400
index = span.IndexOf(value);
@@ -3413,7 +3411,6 @@ public static void IndexOfSequenceLengthOneValueJustPasttVeryEnd_Char()
34133411
Assert.Equal(-1, index);
34143412
Assert.Equal(index, s1.IndexOf(s2, StringComparison.Ordinal));
34153413

3416-
// A zero-length value is always "found" at the start of the span.
34173414
ReadOnlySpan<char> span = s1.AsSpan();
34183415
ReadOnlySpan<char> value = s2.AsSpan();
34193416
index = span.IndexOf(value);
@@ -3887,18 +3884,23 @@ public static void LastIndexOf_AllSubstrings(string s, string value, int startIn
38873884

38883885
if (value.Length == 0)
38893886
{
3890-
int expectedIndex = s.Length > 0 ? s.Length - 1 : 0;
3891-
int expectedStartIndex = startIndex == s.Length ? startIndex - 1 : startIndex;
3887+
int expectedStartIndex = startIndex;
38923888
if (s.Length == 0 && (startIndex == -1 || startIndex == 0))
3893-
expectedStartIndex = (value.Length == 0) ? 0 : -1;
3894-
Assert.Equal(expectedIndex, s.LastIndexOf(value, comparison));
3889+
expectedStartIndex = 0; // empty string occurs at beginning of search space
3890+
if (s.Length > 0 && startIndex < s.Length)
3891+
expectedStartIndex = startIndex + 1; // empty string occurs just after the last char included in the search space
3892+
3893+
Assert.Equal(s.Length, s.LastIndexOf(value, comparison));
38953894
Assert.Equal(expectedStartIndex, s.LastIndexOf(value, startIndex, comparison));
3896-
Assert.Equal(expectedIndex, s.AsSpan().LastIndexOf(value.AsSpan(), comparison));
3895+
Assert.Equal(s.Length, s.AsSpan().LastIndexOf(value.AsSpan(), comparison));
38973896
return;
38983897
}
38993898

39003899
if (s.Length == 0)
39013900
{
3901+
// unit test shouldn't have passed a weightless string to this routine
3902+
Assert.NotEqual(value, string.Empty, StringComparer.FromComparison(comparison));
3903+
39023904
Assert.Equal(-1, s.LastIndexOf(value, comparison));
39033905
Assert.Equal(-1, s.LastIndexOf(value, startIndex, comparison));
39043906
Assert.Equal(-1, s.AsSpan().LastIndexOf(value.AsSpan(), comparison));
@@ -4068,8 +4070,8 @@ public static void LastIndexOf_TurkishI_EnglishUSCulture()
40684070
}
40694071

40704072
[Theory]
4071-
[InlineData("foo", 2)]
4072-
[InlineData("hello", 4)]
4073+
[InlineData("foo", 3)]
4074+
[InlineData("hello", 5)]
40734075
[InlineData("", 0)]
40744076
public static void LastIndexOf_EmptyString(string s, int expected)
40754077
{
@@ -4325,13 +4327,13 @@ public static void LastIndexOfSequenceZeroLengthValue_Char()
43254327
string s1 = "0172377457778667789";
43264328
string s2 = string.Empty;
43274329
int index = s1.LastIndexOf(s2);
4328-
Assert.Equal(s1.Length - 1, index);
4330+
Assert.Equal(s1.Length, index);
43294331

4330-
// A zero-length value is always "found" at the start of the span.
4332+
// A zero-length value is always "found" at the end of the span.
43314333
ReadOnlySpan<char> span = s1.AsSpan();
43324334
ReadOnlySpan<char> value = s2.AsSpan();
43334335
index = span.LastIndexOf(value);
4334-
Assert.Equal(0, index);
4336+
Assert.Equal(span.Length, index);
43354337
}
43364338

43374339
[Fact]
@@ -4356,7 +4358,6 @@ public static void LastIndexOfSequenceLengthOneValue_Char()
43564358
int index = s1.LastIndexOf(s2);
43574359
Assert.Equal(2, index);
43584360

4359-
// A zero-length value is always "found" at the start of the span.
43604361
ReadOnlySpan<char> span = s1.AsSpan();
43614362
ReadOnlySpan<char> value = s2.AsSpan();
43624363
index = span.LastIndexOf(value);
@@ -4371,7 +4372,6 @@ public static void LastIndexOfSequenceLengthOneValueAtVeryEnd_Char()
43714372
int index = s1.LastIndexOf(s2);
43724373
Assert.Equal(5, index);
43734374

4374-
// A zero-length value is always "found" at the start of the span.
43754375
ReadOnlySpan<char> span = s1.AsSpan();
43764376
ReadOnlySpan<char> value = s2.AsSpan();
43774377
index = span.LastIndexOf(value);
@@ -4386,7 +4386,6 @@ public static void LastIndexOfSequenceLengthOneValueMultipleTimes_Char()
43864386
int index = s1.LastIndexOf(s2);
43874387
Assert.Equal(5, index);
43884388

4389-
// A zero-length value is always "found" at the start of the span.
43904389
ReadOnlySpan<char> span = s1.AsSpan();
43914390
ReadOnlySpan<char> value = s2.AsSpan();
43924391
index = span.LastIndexOf(value);
@@ -4401,7 +4400,6 @@ public static void LastIndexOfSequenceLengthOneValueJustPasttVeryEnd_Char()
44014400
int index = s1.LastIndexOf(s2);
44024401
Assert.Equal(-1, index);
44034402

4404-
// A zero-length value is always "found" at the start of the span.
44054403
ReadOnlySpan<char> span = s1.AsSpan();
44064404
ReadOnlySpan<char> value = s2.AsSpan();
44074405
index = span.LastIndexOf(value);

src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.LastIndexOf.cs

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public class CompareInfoLastIndexOfTests
1717
public static IEnumerable<object[]> LastIndexOf_TestData()
1818
{
1919
// Empty strings
20-
yield return new object[] { s_invariantCompare, "foo", "", 2, 3, CompareOptions.None, 2 };
20+
yield return new object[] { s_invariantCompare, "foo", "", 2, 3, CompareOptions.None, 3 };
2121
yield return new object[] { s_invariantCompare, "", "", 0, 0, CompareOptions.None, 0 };
2222
yield return new object[] { s_invariantCompare, "", "a", 0, 0, CompareOptions.None, -1 };
2323
yield return new object[] { s_invariantCompare, "", "", -1, 0, CompareOptions.None, 0 };
@@ -30,8 +30,8 @@ public static IEnumerable<object[]> LastIndexOf_TestData()
3030
yield return new object[] { s_invariantCompare, "Hello", "b", 5, 5, CompareOptions.None, -1 };
3131
yield return new object[] { s_invariantCompare, "Hello", "l", 5, 0, CompareOptions.None, -1 };
3232

33-
yield return new object[] { s_invariantCompare, "Hello", "", 5, 5, CompareOptions.None, 4 };
34-
yield return new object[] { s_invariantCompare, "Hello", "", 5, 0, CompareOptions.None, 4 };
33+
yield return new object[] { s_invariantCompare, "Hello", "", 5, 5, CompareOptions.None, 5 };
34+
yield return new object[] { s_invariantCompare, "Hello", "", 5, 0, CompareOptions.None, 5 };
3535

3636
// OrdinalIgnoreCase
3737
yield return new object[] { s_invariantCompare, "Hello", "l", 4, 5, CompareOptions.OrdinalIgnoreCase, 3 };
@@ -157,6 +157,32 @@ public void LastIndexOf_String(CompareInfo compareInfo, string source, string va
157157
// Use LastIndexOf(string, string, int, int, CompareOptions)
158158
Assert.Equal(expected, compareInfo.LastIndexOf(source, value, startIndex, count, options));
159159

160+
// Fixup offsets so that we can call the span-based APIs.
161+
162+
ReadOnlySpan<char> sourceSpan;
163+
int adjustmentFactor; // number of chars to add to retured index from span-based APIs
164+
165+
if (startIndex == source.Length - 1 && count == source.Length)
166+
{
167+
// This idiom means "read the whole span"
168+
sourceSpan = source;
169+
adjustmentFactor = 0;
170+
}
171+
else if (startIndex == source.Length)
172+
{
173+
// Account for possible off-by-one at the call site
174+
sourceSpan = source.AsSpan()[^(Math.Max(0, count - 1))..];
175+
adjustmentFactor = source.Length - sourceSpan.Length;
176+
}
177+
else
178+
{
179+
// Bump 'startIndex' by 1, then go back 'count' chars
180+
sourceSpan = source.AsSpan()[..(startIndex + 1)][^count..];
181+
adjustmentFactor = startIndex + 1 - count;
182+
}
183+
184+
if (expected < 0) { adjustmentFactor = 0; } // don't modify "not found" (-1) return values
185+
160186
if ((compareInfo == s_invariantCompare) && ((options == CompareOptions.None) || (options == CompareOptions.IgnoreCase)))
161187
{
162188
StringComparison stringComparison = (options == CompareOptions.IgnoreCase) ? StringComparison.InvariantCultureIgnoreCase : StringComparison.InvariantCulture;
@@ -165,20 +191,7 @@ public void LastIndexOf_String(CompareInfo compareInfo, string source, string va
165191
Assert.Equal(expected, source.LastIndexOf(value, startIndex, count, stringComparison));
166192

167193
// Use int MemoryExtensions.LastIndexOf(this ReadOnlySpan<char>, ReadOnlySpan<char>, StringComparison)
168-
// Filter differences betweeen string-based and Span-based LastIndexOf
169-
// - Empty value handling - https://github.com/dotnet/runtime/issues/13382
170-
// - Negative count
171-
if (value.Length == 0 || count < 0)
172-
return;
173-
174-
if (startIndex == source.Length)
175-
{
176-
startIndex--;
177-
if (count > 0)
178-
count--;
179-
}
180-
int leftStartIndex = (startIndex - count + 1);
181-
Assert.Equal((expected == -1) ? -1 : (expected - leftStartIndex), source.AsSpan(leftStartIndex, count).LastIndexOf(value.AsSpan(), stringComparison));
194+
Assert.Equal(expected - adjustmentFactor, sourceSpan.LastIndexOf(value.AsSpan(), stringComparison));
182195
}
183196
}
184197

src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.cs

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ public static IEnumerable<object[]> SortKey_TestData()
310310

311311
public static IEnumerable<object[]> IndexOf_TestData()
312312
{
313-
yield return new object[] { s_invariantCompare, "foo", "", 0, 0, 0 };
313+
yield return new object[] { s_invariantCompare, "foo", "", 0, 0, 1 };
314314
yield return new object[] { s_invariantCompare, "", "", 0, 0, 0 };
315315
yield return new object[] { s_invariantCompare, "Hello", "l", 0, 2, -1 };
316316
yield return new object[] { s_invariantCompare, "Hello", "l", 3, 3, 3 };
@@ -321,10 +321,14 @@ public static IEnumerable<object[]> IndexOf_TestData()
321321

322322
public static IEnumerable<object[]> IsSortable_TestData()
323323
{
324-
yield return new object[] { "", false, false };
325-
yield return new object[] { "abcdefg", false, true };
326-
yield return new object[] { "\uD800\uDC00", true, true };
327-
yield return new object[] { "\uD800\uD800", true, false };
324+
yield return new object[] { "", false };
325+
yield return new object[] { "abcdefg", true };
326+
yield return new object[] { "\uD800\uDC00", true };
327+
328+
// VS test runner for xunit doesn't handle ill-formed UTF-16 strings properly.
329+
// We'll send this one through as an array to avoid U+FFFD substitution.
330+
331+
yield return new object[] { new char[] { '\uD800', '\uD800' }, false };
328332
}
329333

330334
[Theory]
@@ -413,27 +417,30 @@ public void SortKeyMiscTest()
413417
public void IndexOfTest(CompareInfo compareInfo, string source, string value, int startIndex, int indexOfExpected, int lastIndexOfExpected)
414418
{
415419
Assert.Equal(indexOfExpected, compareInfo.IndexOf(source, value, startIndex));
416-
if (value.Length > 0)
420+
if (value.Length == 1)
417421
{
418422
Assert.Equal(indexOfExpected, compareInfo.IndexOf(source, value[0], startIndex));
419423
}
420424

421425
Assert.Equal(lastIndexOfExpected, compareInfo.LastIndexOf(source, value, startIndex));
422-
if (value.Length > 0)
426+
if (value.Length == 1)
423427
{
424428
Assert.Equal(lastIndexOfExpected, compareInfo.LastIndexOf(source, value[0], startIndex));
425429
}
426430
}
427431

428432
[Theory]
429433
[MemberData(nameof(IsSortable_TestData))]
430-
public void IsSortableTest(string source, bool hasSurrogate, bool expected)
434+
public void IsSortableTest(object sourceObj, bool expected)
431435
{
436+
string source = sourceObj as string ?? new string((char[])sourceObj);
432437
Assert.Equal(expected, CompareInfo.IsSortable(source));
433438

434-
bool charExpectedResults = hasSurrogate ? false : expected;
439+
// If the string as a whole is sortable, then all chars which aren't standalone
440+
// surrogate halves must also be sortable.
441+
435442
foreach (char c in source)
436-
Assert.Equal(charExpectedResults, CompareInfo.IsSortable(c));
443+
Assert.Equal(expected && !char.IsSurrogate(c), CompareInfo.IsSortable(c));
437444
}
438445

439446
[Fact]

0 commit comments

Comments
 (0)