Fix various LastIndexOf bugs when given zero-length target values (#34616)
authorLevi Broderick <GrabYourPitchforks@users.noreply.github.com>
Tue, 7 Apr 2020 19:16:24 +0000 (12:16 -0700)
committerGitHub <noreply@github.com>
Tue, 7 Apr 2020 19:16:24 +0000 (12:16 -0700)
- 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

24 files changed:
src/libraries/Common/tests/Tests/System/StringTests.cs
src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.LastIndexOf.cs
src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.cs
src/libraries/System.Globalization/tests/Invariant/InvariantMode.cs
src/libraries/System.Memory/tests/ReadOnlySpan/IndexOfSequence.T.cs
src/libraries/System.Memory/tests/ReadOnlySpan/IndexOfSequence.byte.cs
src/libraries/System.Memory/tests/ReadOnlySpan/LastIndexOfSequence.T.cs
src/libraries/System.Memory/tests/ReadOnlySpan/LastIndexOfSequence.byte.cs
src/libraries/System.Memory/tests/Span/IndexOfSequence.T.cs
src/libraries/System.Memory/tests/Span/IndexOfSequence.byte.cs
src/libraries/System.Memory/tests/Span/IndexOfSequence.char.cs
src/libraries/System.Memory/tests/Span/LastIndexOfSequence.T.cs
src/libraries/System.Memory/tests/Span/LastIndexOfSequence.byte.cs
src/libraries/System.Memory/tests/Span/LastIndexOfSequence.char.cs
src/libraries/System.Memory/tests/TestHelpers.cs
src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Invariant.cs
src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Unix.cs
src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Windows.cs
src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.cs
src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.Globalization.cs
src/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs
src/libraries/System.Private.CoreLib/src/System/SpanHelpers.T.cs
src/libraries/System.Private.CoreLib/src/System/String.Comparison.cs
src/libraries/System.Private.CoreLib/src/System/String.Searching.cs

index fd24ad5..ba1757f 100644 (file)
@@ -3379,7 +3379,6 @@ namespace System.Tests
             Assert.Equal(2, index);
             Assert.Equal(index, s1.IndexOf(s2, StringComparison.Ordinal));
 
-            // A zero-length value is always "found" at the start of the span.
             ReadOnlySpan<char> span = s1.AsSpan();
             ReadOnlySpan<char> value = s2.AsSpan();
             index = span.IndexOf(value);
@@ -3396,7 +3395,6 @@ namespace System.Tests
             Assert.Equal(5, index);
             Assert.Equal(index, s1.IndexOf(s2, StringComparison.Ordinal));
 
-            // A zero-length value is always "found" at the start of the span.
             ReadOnlySpan<char> span = s1.AsSpan();
             ReadOnlySpan<char> value = s2.AsSpan();
             index = span.IndexOf(value);
@@ -3413,7 +3411,6 @@ namespace System.Tests
             Assert.Equal(-1, index);
             Assert.Equal(index, s1.IndexOf(s2, StringComparison.Ordinal));
 
-            // A zero-length value is always "found" at the start of the span.
             ReadOnlySpan<char> span = s1.AsSpan();
             ReadOnlySpan<char> value = s2.AsSpan();
             index = span.IndexOf(value);
@@ -3887,18 +3884,23 @@ namespace System.Tests
 
             if (value.Length == 0)
             {
-                int expectedIndex = s.Length > 0 ? s.Length - 1 : 0;
-                int expectedStartIndex = startIndex == s.Length ? startIndex - 1 : startIndex;
+                int expectedStartIndex = startIndex;
                 if (s.Length == 0 && (startIndex == -1 || startIndex == 0))
-                    expectedStartIndex = (value.Length == 0) ? 0 : -1;
-                Assert.Equal(expectedIndex, s.LastIndexOf(value, comparison));
+                    expectedStartIndex = 0; // empty string occurs at beginning of search space
+                if (s.Length > 0 && startIndex < s.Length)
+                    expectedStartIndex = startIndex + 1; // empty string occurs just after the last char included in the search space
+
+                Assert.Equal(s.Length, s.LastIndexOf(value, comparison));
                 Assert.Equal(expectedStartIndex, s.LastIndexOf(value, startIndex, comparison));
-                Assert.Equal(expectedIndex, s.AsSpan().LastIndexOf(value.AsSpan(), comparison));
+                Assert.Equal(s.Length, s.AsSpan().LastIndexOf(value.AsSpan(), comparison));
                 return;
             }
 
             if (s.Length == 0)
             {
+                // unit test shouldn't have passed a weightless string to this routine
+                Assert.NotEqual(value, string.Empty, StringComparer.FromComparison(comparison));
+
                 Assert.Equal(-1, s.LastIndexOf(value, comparison));
                 Assert.Equal(-1, s.LastIndexOf(value, startIndex, comparison));
                 Assert.Equal(-1, s.AsSpan().LastIndexOf(value.AsSpan(), comparison));
@@ -4068,8 +4070,8 @@ namespace System.Tests
         }
 
         [Theory]
-        [InlineData("foo", 2)]
-        [InlineData("hello", 4)]
+        [InlineData("foo", 3)]
+        [InlineData("hello", 5)]
         [InlineData("", 0)]
         public static void LastIndexOf_EmptyString(string s, int expected)
         {
@@ -4325,13 +4327,13 @@ namespace System.Tests
             string s1 = "0172377457778667789";
             string s2 = string.Empty;
             int index = s1.LastIndexOf(s2);
-            Assert.Equal(s1.Length - 1, index);
+            Assert.Equal(s1.Length, index);
 
-            // A zero-length value is always "found" at the start of the span.
+            // A zero-length value is always "found" at the end of the span.
             ReadOnlySpan<char> span = s1.AsSpan();
             ReadOnlySpan<char> value = s2.AsSpan();
             index = span.LastIndexOf(value);
-            Assert.Equal(0, index);
+            Assert.Equal(span.Length, index);
         }
 
         [Fact]
@@ -4356,7 +4358,6 @@ namespace System.Tests
             int index = s1.LastIndexOf(s2);
             Assert.Equal(2, index);
 
-            // A zero-length value is always "found" at the start of the span.
             ReadOnlySpan<char> span = s1.AsSpan();
             ReadOnlySpan<char> value = s2.AsSpan();
             index = span.LastIndexOf(value);
@@ -4371,7 +4372,6 @@ namespace System.Tests
             int index = s1.LastIndexOf(s2);
             Assert.Equal(5, index);
 
-            // A zero-length value is always "found" at the start of the span.
             ReadOnlySpan<char> span = s1.AsSpan();
             ReadOnlySpan<char> value = s2.AsSpan();
             index = span.LastIndexOf(value);
@@ -4386,7 +4386,6 @@ namespace System.Tests
             int index = s1.LastIndexOf(s2);
             Assert.Equal(5, index);
 
-            // A zero-length value is always "found" at the start of the span.
             ReadOnlySpan<char> span = s1.AsSpan();
             ReadOnlySpan<char> value = s2.AsSpan();
             index = span.LastIndexOf(value);
@@ -4401,7 +4400,6 @@ namespace System.Tests
             int index = s1.LastIndexOf(s2);
             Assert.Equal(-1, index);
 
-            // A zero-length value is always "found" at the start of the span.
             ReadOnlySpan<char> span = s1.AsSpan();
             ReadOnlySpan<char> value = s2.AsSpan();
             index = span.LastIndexOf(value);
index 459f0b7..4d0474e 100644 (file)
@@ -17,7 +17,7 @@ namespace System.Globalization.Tests
         public static IEnumerable<object[]> LastIndexOf_TestData()
         {
             // Empty strings
-            yield return new object[] { s_invariantCompare, "foo", "", 2, 3, CompareOptions.None, 2 };
+            yield return new object[] { s_invariantCompare, "foo", "", 2, 3, CompareOptions.None, 3 };
             yield return new object[] { s_invariantCompare, "", "", 0, 0, CompareOptions.None, 0 };
             yield return new object[] { s_invariantCompare, "", "a", 0, 0, CompareOptions.None, -1 };
             yield return new object[] { s_invariantCompare, "", "", -1, 0, CompareOptions.None, 0 };
@@ -30,8 +30,8 @@ namespace System.Globalization.Tests
             yield return new object[] { s_invariantCompare, "Hello", "b", 5, 5, CompareOptions.None, -1 };
             yield return new object[] { s_invariantCompare, "Hello", "l", 5, 0, CompareOptions.None, -1 };
 
-            yield return new object[] { s_invariantCompare, "Hello", "", 5, 5, CompareOptions.None, 4 };
-            yield return new object[] { s_invariantCompare, "Hello", "", 5, 0, CompareOptions.None, 4 };
+            yield return new object[] { s_invariantCompare, "Hello", "", 5, 5, CompareOptions.None, 5 };
+            yield return new object[] { s_invariantCompare, "Hello", "", 5, 0, CompareOptions.None, 5 };
 
             // OrdinalIgnoreCase
             yield return new object[] { s_invariantCompare, "Hello", "l", 4, 5, CompareOptions.OrdinalIgnoreCase, 3 };
@@ -157,6 +157,32 @@ namespace System.Globalization.Tests
             // Use LastIndexOf(string, string, int, int, CompareOptions)
             Assert.Equal(expected, compareInfo.LastIndexOf(source, value, startIndex, count, options));
 
+            // Fixup offsets so that we can call the span-based APIs.
+
+            ReadOnlySpan<char> sourceSpan;
+            int adjustmentFactor; // number of chars to add to retured index from span-based APIs
+
+            if (startIndex == source.Length - 1 && count == source.Length)
+            {
+                // This idiom means "read the whole span"
+                sourceSpan = source;
+                adjustmentFactor = 0;
+            }
+            else if (startIndex == source.Length)
+            {
+                // Account for possible off-by-one at the call site
+                sourceSpan = source.AsSpan()[^(Math.Max(0, count - 1))..];
+                adjustmentFactor = source.Length - sourceSpan.Length;
+            }
+            else
+            {
+                // Bump 'startIndex' by 1, then go back 'count' chars
+                sourceSpan = source.AsSpan()[..(startIndex + 1)][^count..];
+                adjustmentFactor = startIndex + 1 - count;
+            }
+
+            if (expected < 0) { adjustmentFactor = 0; } // don't modify "not found" (-1) return values
+
             if ((compareInfo == s_invariantCompare) && ((options == CompareOptions.None) || (options == CompareOptions.IgnoreCase)))
             {
                 StringComparison stringComparison = (options == CompareOptions.IgnoreCase) ? StringComparison.InvariantCultureIgnoreCase : StringComparison.InvariantCulture;
@@ -165,20 +191,7 @@ namespace System.Globalization.Tests
                 Assert.Equal(expected, source.LastIndexOf(value, startIndex, count, stringComparison));
 
                 // Use int MemoryExtensions.LastIndexOf(this ReadOnlySpan<char>, ReadOnlySpan<char>, StringComparison)
-                // Filter differences betweeen string-based and Span-based LastIndexOf
-                // - Empty value handling - https://github.com/dotnet/runtime/issues/13382
-                // - Negative count
-                if (value.Length == 0 || count < 0)
-                    return;
-
-                if (startIndex == source.Length)
-                {
-                    startIndex--;
-                    if (count > 0)
-                        count--;
-                }
-                int leftStartIndex = (startIndex - count + 1);
-                Assert.Equal((expected == -1) ? -1 : (expected - leftStartIndex), source.AsSpan(leftStartIndex, count).LastIndexOf(value.AsSpan(), stringComparison));
+                Assert.Equal(expected - adjustmentFactor, sourceSpan.LastIndexOf(value.AsSpan(), stringComparison));
             }
         }
 
index d319c69..9967bd4 100644 (file)
@@ -310,7 +310,7 @@ namespace System.Globalization.Tests
 
         public static IEnumerable<object[]> IndexOf_TestData()
         {
-            yield return new object[] { s_invariantCompare, "foo", "", 0,  0, 0 };
+            yield return new object[] { s_invariantCompare, "foo", "", 0, 0, 1 };
             yield return new object[] { s_invariantCompare, "", "", 0, 0, 0 };
             yield return new object[] { s_invariantCompare, "Hello", "l", 0,  2, -1 };
             yield return new object[] { s_invariantCompare, "Hello", "l", 3,  3, 3 };
@@ -321,10 +321,14 @@ namespace System.Globalization.Tests
 
         public static IEnumerable<object[]> IsSortable_TestData()
         {
-            yield return new object[] { "", false, false };
-            yield return new object[] { "abcdefg",  false, true };
-            yield return new object[] { "\uD800\uDC00", true,  true };
-            yield return new object[] { "\uD800\uD800", true,  false };
+            yield return new object[] { "", false };
+            yield return new object[] { "abcdefg", true };
+            yield return new object[] { "\uD800\uDC00", true };
+
+            // VS test runner for xunit doesn't handle ill-formed UTF-16 strings properly.
+            // We'll send this one through as an array to avoid U+FFFD substitution.
+
+            yield return new object[] { new char[] { '\uD800', '\uD800' }, false };
         }
 
         [Theory]
@@ -413,13 +417,13 @@ namespace System.Globalization.Tests
         public void IndexOfTest(CompareInfo compareInfo, string source, string value, int startIndex, int indexOfExpected, int lastIndexOfExpected)
         {
             Assert.Equal(indexOfExpected, compareInfo.IndexOf(source, value, startIndex));
-            if (value.Length > 0)
+            if (value.Length == 1)
             {
                 Assert.Equal(indexOfExpected, compareInfo.IndexOf(source, value[0], startIndex));
             }
 
             Assert.Equal(lastIndexOfExpected, compareInfo.LastIndexOf(source, value, startIndex));
-            if (value.Length > 0)
+            if (value.Length == 1)
             {
                 Assert.Equal(lastIndexOfExpected, compareInfo.LastIndexOf(source, value[0], startIndex));
             }
@@ -427,13 +431,16 @@ namespace System.Globalization.Tests
 
         [Theory]
         [MemberData(nameof(IsSortable_TestData))]
-        public void IsSortableTest(string source, bool hasSurrogate, bool expected)
+        public void IsSortableTest(object sourceObj, bool expected)
         {
+            string source = sourceObj as string ?? new string((char[])sourceObj);
             Assert.Equal(expected, CompareInfo.IsSortable(source));
 
-            bool charExpectedResults = hasSurrogate ? false : expected;
+            // If the string as a whole is sortable, then all chars which aren't standalone
+            // surrogate halves must also be sortable.
+
             foreach (char c in source)
-                Assert.Equal(charExpectedResults, CompareInfo.IsSortable(c));
+                Assert.Equal(expected && !char.IsSurrogate(c), CompareInfo.IsSortable(c));
         }
 
         [Fact]
index 5291add..d6c7676 100644 (file)
@@ -2,6 +2,7 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 // See the LICENSE file in the project root for more information.
 
+using System.Buffers;
 using System.Collections.Generic;
 using System.Collections;
 using System.Text;
@@ -114,7 +115,7 @@ namespace System.Globalization.Tests
         public static IEnumerable<object[]> LastIndexOf_TestData()
         {
             // Empty strings
-            yield return new object[] { "foo", "", 2, 3, CompareOptions.None, 2 };
+            yield return new object[] { "foo", "", 2, 3, CompareOptions.None, 3 };
             yield return new object[] { "", "", 0, 0, CompareOptions.None, 0 };
             yield return new object[] { "", "a", 0, 0, CompareOptions.None, -1 };
             yield return new object[] { "", "", -1, 0, CompareOptions.None, 0 };
@@ -127,8 +128,8 @@ namespace System.Globalization.Tests
             yield return new object[] { "Hello", "b", 5, 5, CompareOptions.None, -1 };
             yield return new object[] { "Hello", "l", 5, 0, CompareOptions.None, -1 };
 
-            yield return new object[] { "Hello", "", 5, 5, CompareOptions.None, 4 };
-            yield return new object[] { "Hello", "", 5, 0, CompareOptions.None, 4 };
+            yield return new object[] { "Hello", "", 5, 5, CompareOptions.None, 5 };
+            yield return new object[] { "Hello", "", 5, 0, CompareOptions.None, 5 };
 
             // OrdinalIgnoreCase
             yield return new object[] { "Hello", "l", 4, 5, CompareOptions.OrdinalIgnoreCase, 3 };
@@ -181,6 +182,9 @@ namespace System.Globalization.Tests
             yield return new object[] { "foo", "", CompareOptions.None, true };
             yield return new object[] { "", "", CompareOptions.None, true };
 
+            // Early exit for empty values before 'options' is validated
+            yield return new object[] { "hello", "", (CompareOptions)(-1), true };
+
             // Long strings
             yield return new object[] { new string('a', 5555), "aaaaaaaaaaaaaaa", CompareOptions.None, true };
             yield return new object[] { new string('a', 5555), new string('a', 5000), CompareOptions.None, true };
@@ -219,6 +223,9 @@ namespace System.Globalization.Tests
             yield return new object[] { "foo", "", CompareOptions.None, true };
             yield return new object[] { "", "", CompareOptions.None, true };
 
+            // Early exit for empty values before 'options' is validated
+            yield return new object[] { "hello", "", (CompareOptions)(-1), true };
+
             // Long strings
             yield return new object[] { new string('a', 5555), "aaaaaaaaaaaaaaa", CompareOptions.None, true };
             yield return new object[] { new string('a', 5555), new string('a', 5000), CompareOptions.None, true };
@@ -726,7 +733,18 @@ namespace System.Globalization.Tests
             {
                 Assert.Equal(result, CultureInfo.GetCultureInfo(cul).CompareInfo.IsPrefix(source, value, options));
                 Assert.Equal(result, source.StartsWith(value, GetStringComparison(options)));
-                Assert.Equal(result, source.AsSpan().StartsWith(value.AsSpan(), GetStringComparison(options)));
+
+                // Span versions - using BoundedMemory to check for buffer overruns
+
+                using BoundedMemory<char> sourceBoundedMemory = BoundedMemory.AllocateFromExistingData<char>(source);
+                sourceBoundedMemory.MakeReadonly();
+                ReadOnlySpan<char> sourceBoundedSpan = sourceBoundedMemory.Span;
+
+                using BoundedMemory<char> valueBoundedMemory = BoundedMemory.AllocateFromExistingData<char>(value);
+                valueBoundedMemory.MakeReadonly();
+                ReadOnlySpan<char> valueBoundedSpan = valueBoundedMemory.Span;
+
+                Assert.Equal(result, sourceBoundedSpan.StartsWith(valueBoundedSpan, GetStringComparison(options)));
             }
         }
 
@@ -738,10 +756,37 @@ namespace System.Globalization.Tests
             {
                 Assert.Equal(result, CultureInfo.GetCultureInfo(cul).CompareInfo.IsSuffix(source, value, options));
                 Assert.Equal(result, source.EndsWith(value, GetStringComparison(options)));
-                Assert.Equal(result, source.AsSpan().EndsWith(value.AsSpan(), GetStringComparison(options)));
+
+                // Span versions - using BoundedMemory to check for buffer overruns
+
+                using BoundedMemory<char> sourceBoundedMemory = BoundedMemory.AllocateFromExistingData<char>(source);
+                sourceBoundedMemory.MakeReadonly();
+                ReadOnlySpan<char> sourceBoundedSpan = sourceBoundedMemory.Span;
+
+                using BoundedMemory<char> valueBoundedMemory = BoundedMemory.AllocateFromExistingData<char>(value);
+                valueBoundedMemory.MakeReadonly();
+                ReadOnlySpan<char> valueBoundedSpan = valueBoundedMemory.Span;
+
+                Assert.Equal(result, sourceBoundedSpan.EndsWith(valueBoundedSpan, GetStringComparison(options)));
             }
         }
 
+        [Theory]
+        [InlineData("", false)]
+        [InlineData('x', true)]
+        [InlineData('\ud800', true)] // standalone high surrogate
+        [InlineData("hello", true)]
+        public void TestIsSortable(object sourceObj, bool expectedResult)
+        {
+            if (sourceObj is string s)
+            {
+                Assert.Equal(expectedResult, CompareInfo.IsSortable(s));
+            }
+            else
+            {
+                Assert.Equal(expectedResult, CompareInfo.IsSortable((char)sourceObj));
+            }
+        }
 
         [Theory]
         [MemberData(nameof(Compare_TestData))]
@@ -750,13 +795,23 @@ namespace System.Globalization.Tests
             foreach (string cul in s_cultureNames)
             {
                 int res = CultureInfo.GetCultureInfo(cul).CompareInfo.Compare(source, value, options);
-                if (res < 0) res = -1;
-                if (res > 0) res = 1;
-                Assert.Equal(result, res);
+                Assert.Equal(result, Math.Sign(res));
+
                 res = string.Compare(source, value, GetStringComparison(options));
-                if (res < 0) res = -1;
-                if (res > 0) res = 1;
-                Assert.Equal(result, res);
+                Assert.Equal(result, Math.Sign(res));
+
+                // Span versions - using BoundedMemory to check for buffer overruns
+
+                using BoundedMemory<char> sourceBoundedMemory = BoundedMemory.AllocateFromExistingData<char>(source);
+                sourceBoundedMemory.MakeReadonly();
+                ReadOnlySpan<char> sourceBoundedSpan = sourceBoundedMemory.Span;
+
+                using BoundedMemory<char> valueBoundedMemory = BoundedMemory.AllocateFromExistingData<char>(value);
+                valueBoundedMemory.MakeReadonly();
+                ReadOnlySpan<char> valueBoundedSpan = valueBoundedMemory.Span;
+
+                res = sourceBoundedSpan.CompareTo(valueBoundedSpan, GetStringComparison(options));
+                Assert.Equal(result, Math.Sign(res));
             }
         }
 
index cb0fd3a..85d73a8 100644 (file)
@@ -93,7 +93,6 @@ namespace System.SpanTests
         [Fact]
         public static void IndexOfSequenceLengthOneValue()
         {
-            // A zero-length value is always "found" at the start of the span.
             ReadOnlySpan<int> span = new ReadOnlySpan<int>(new int[] { 0, 1, 2, 3, 4, 5 });
             ReadOnlySpan<int> value = new ReadOnlySpan<int>(new int[] { 2 });
             int index = span.IndexOf(value);
@@ -103,7 +102,6 @@ namespace System.SpanTests
         [Fact]
         public static void IndexOfSequenceLengthOneValueAtVeryEnd()
         {
-            // A zero-length value is always "found" at the start of the span.
             ReadOnlySpan<int> span = new ReadOnlySpan<int>(new int[] { 0, 1, 2, 3, 4, 5 });
             ReadOnlySpan<int> value = new ReadOnlySpan<int>(new int[] { 5 });
             int index = span.IndexOf(value);
@@ -113,7 +111,6 @@ namespace System.SpanTests
         [Fact]
         public static void IndexOfSequenceLengthOneValueJustPasttVeryEnd()
         {
-            // A zero-length value is always "found" at the start of the span.
             ReadOnlySpan<int> span = new ReadOnlySpan<int>(new int[] { 0, 1, 2, 3, 4, 5 }, 0, 5);
             ReadOnlySpan<int> value = new ReadOnlySpan<int>(new int[] { 5 });
             int index = span.IndexOf(value);
@@ -205,7 +202,6 @@ namespace System.SpanTests
         [Fact]
         public static void IndexOfSequenceLengthOneValue_String()
         {
-            // A zero-length value is always "found" at the start of the span.
             ReadOnlySpan<string> span = new ReadOnlySpan<string>(new string[] { "0", "1", "2", "3", "4", "5" });
             ReadOnlySpan<string> value = new ReadOnlySpan<string>(new string[] { "2" });
             int index = span.IndexOf(value);
@@ -215,7 +211,6 @@ namespace System.SpanTests
         [Fact]
         public static void IndexOfSequenceLengthOneValueAtVeryEnd_String()
         {
-            // A zero-length value is always "found" at the start of the span.
             ReadOnlySpan<string> span = new ReadOnlySpan<string>(new string[] { "0", "1", "2", "3", "4", "5" });
             ReadOnlySpan<string> value = new ReadOnlySpan<string>(new string[] { "5" });
             int index = span.IndexOf(value);
@@ -225,7 +220,6 @@ namespace System.SpanTests
         [Fact]
         public static void IndexOfSequenceLengthOneValueJustPasttVeryEnd_String()
         {
-            // A zero-length value is always "found" at the start of the span.
             ReadOnlySpan<string> span = new ReadOnlySpan<string>(new string[] { "0", "1", "2", "3", "4", "5" }, 0, 5);
             ReadOnlySpan<string> value = new ReadOnlySpan<string>(new string[] { "5" });
             int index = span.IndexOf(value);
index 3a23695..c823d97 100644 (file)
@@ -93,7 +93,6 @@ namespace System.SpanTests
         [Fact]
         public static void IndexOfSequenceLengthOneValue_Byte()
         {
-            // A zero-length value is always "found" at the start of the span.
             ReadOnlySpan<byte> span = new ReadOnlySpan<byte>(new byte[] { 0, 1, 2, 3, 4, 5 });
             ReadOnlySpan<byte> value = new ReadOnlySpan<byte>(new byte[] { 2 });
             int index = span.IndexOf(value);
@@ -103,7 +102,6 @@ namespace System.SpanTests
         [Fact]
         public static void IndexOfSequenceLengthOneValueAtVeryEnd_Byte()
         {
-            // A zero-length value is always "found" at the start of the span.
             ReadOnlySpan<byte> span = new ReadOnlySpan<byte>(new byte[] { 0, 1, 2, 3, 4, 5 });
             ReadOnlySpan<byte> value = new ReadOnlySpan<byte>(new byte[] { 5 });
             int index = span.IndexOf(value);
@@ -113,7 +111,6 @@ namespace System.SpanTests
         [Fact]
         public static void IndexOfSequenceLengthOneValueJustPasttVeryEnd_Byte()
         {
-            // A zero-length value is always "found" at the start of the span.
             ReadOnlySpan<byte> span = new ReadOnlySpan<byte>(new byte[] { 0, 1, 2, 3, 4, 5 }, 0, 5);
             ReadOnlySpan<byte> value = new ReadOnlySpan<byte>(new byte[] { 5 });
             int index = span.IndexOf(value);
index 667d2b7..22556e3 100644 (file)
@@ -74,11 +74,11 @@ namespace System.SpanTests
         [Fact]
         public static void LastIndexOfSequenceZeroLengthValue()
         {
-            // A zero-length value is always "found" at the start of the span.
+            // A zero-length value is always "found" at the end of the span.
             ReadOnlySpan<int> span = new ReadOnlySpan<int>(new int[] { 0, 1, 77, 2, 3, 77, 77, 4, 5, 77, 77, 77, 88, 6, 6, 77, 77, 88, 9 });
             ReadOnlySpan<int> value = new ReadOnlySpan<int>(Array.Empty<int>());
             int index = span.LastIndexOf(value);
-            Assert.Equal(0, index);
+            Assert.Equal(span.Length, index);
         }
 
         [Fact]
@@ -93,7 +93,6 @@ namespace System.SpanTests
         [Fact]
         public static void LastIndexOfSequenceLengthOneValue()
         {
-            // A zero-length value is always "found" at the start of the span.
             ReadOnlySpan<int> span = new ReadOnlySpan<int>(new int[] { 0, 1, 2, 3, 4, 5 });
             ReadOnlySpan<int> value = new ReadOnlySpan<int>(new int[] { 2 });
             int index = span.LastIndexOf(value);
@@ -103,7 +102,6 @@ namespace System.SpanTests
         [Fact]
         public static void LastIndexOfSequenceLengthOneValueAtVeryEnd()
         {
-            // A zero-length value is always "found" at the start of the span.
             ReadOnlySpan<int> span = new ReadOnlySpan<int>(new int[] { 0, 1, 2, 3, 4, 5 });
             ReadOnlySpan<int> value = new ReadOnlySpan<int>(new int[] { 5 });
             int index = span.LastIndexOf(value);
@@ -113,7 +111,6 @@ namespace System.SpanTests
         [Fact]
         public static void LastIndexOfSequenceLengthOneValueMultipleTimes()
         {
-            // A zero-length value is always "found" at the start of the span.
             ReadOnlySpan<int> span = new ReadOnlySpan<int>(new int[] { 0, 1, 5, 3, 4, 5 });
             ReadOnlySpan<int> value = new ReadOnlySpan<int>(new int[] { 5 });
             int index = span.LastIndexOf(value);
@@ -123,7 +120,6 @@ namespace System.SpanTests
         [Fact]
         public static void LastIndexOfSequenceLengthOneValueJustPasttVeryEnd()
         {
-            // A zero-length value is always "found" at the start of the span.
             ReadOnlySpan<int> span = new ReadOnlySpan<int>(new int[] { 0, 1, 2, 3, 4, 5 }, 0, 5);
             ReadOnlySpan<int> value = new ReadOnlySpan<int>(new int[] { 5 });
             int index = span.LastIndexOf(value);
@@ -196,11 +192,11 @@ namespace System.SpanTests
         [Fact]
         public static void LastIndexOfSequenceZeroLengthValue_String()
         {
-            // A zero-length value is always "found" at the start of the span.
+            // A zero-length value is always "found" at the end of the span.
             ReadOnlySpan<string> span = new ReadOnlySpan<string>(new string[] { "0", "1", "77", "2", "3", "77", "77", "4", "5", "77", "77", "77", "88", "6", "6", "77", "77", "88", "9" });
             ReadOnlySpan<string> value = new ReadOnlySpan<string>(Array.Empty<string>());
             int index = span.LastIndexOf(value);
-            Assert.Equal(0, index);
+            Assert.Equal(span.Length, index);
         }
 
         [Fact]
@@ -215,7 +211,6 @@ namespace System.SpanTests
         [Fact]
         public static void LastIndexOfSequenceLengthOneValue_String()
         {
-            // A zero-length value is always "found" at the start of the span.
             ReadOnlySpan<string> span = new ReadOnlySpan<string>(new string[] { "0", "1", "2", "3", "4", "5" });
             ReadOnlySpan<string> value = new ReadOnlySpan<string>(new string[] { "2" });
             int index = span.LastIndexOf(value);
@@ -225,7 +220,6 @@ namespace System.SpanTests
         [Fact]
         public static void LastIndexOfSequenceLengthOneValueAtVeryEnd_String()
         {
-            // A zero-length value is always "found" at the start of the span.
             ReadOnlySpan<string> span = new ReadOnlySpan<string>(new string[] { "0", "1", "2", "3", "4", "5" });
             ReadOnlySpan<string> value = new ReadOnlySpan<string>(new string[] { "5" });
             int index = span.LastIndexOf(value);
@@ -235,7 +229,6 @@ namespace System.SpanTests
         [Fact]
         public static void LastIndexOfSequenceLengthOneValueJustPasttVeryEnd_String()
         {
-            // A zero-length value is always "found" at the start of the span.
             ReadOnlySpan<string> span = new ReadOnlySpan<string>(new string[] { "0", "1", "2", "3", "4", "5" }, 0, 5);
             ReadOnlySpan<string> value = new ReadOnlySpan<string>(new string[] { "5" });
             int index = span.LastIndexOf(value);
index 293920a..1e36c1c 100644 (file)
@@ -74,11 +74,11 @@ namespace System.SpanTests
         [Fact]
         public static void LastIndexOfSequenceZeroLengthValue_Byte()
         {
-            // A zero-length value is always "found" at the start of the span.
+            // A zero-length value is always "found" at the end of the span.
             ReadOnlySpan<byte> span = new ReadOnlySpan<byte>(new byte[] { 0, 1, 77, 2, 3, 77, 77, 4, 5, 77, 77, 77, 88, 6, 6, 77, 77, 88, 9 });
             ReadOnlySpan<byte> value = new ReadOnlySpan<byte>(Array.Empty<byte>());
             int index = span.LastIndexOf(value);
-            Assert.Equal(0, index);
+            Assert.Equal(span.Length, index);
         }
 
         [Fact]
@@ -93,7 +93,6 @@ namespace System.SpanTests
         [Fact]
         public static void LastIndexOfSequenceLengthOneValue_Byte()
         {
-            // A zero-length value is always "found" at the start of the span.
             ReadOnlySpan<byte> span = new ReadOnlySpan<byte>(new byte[] { 0, 1, 2, 3, 4, 5 });
             ReadOnlySpan<byte> value = new ReadOnlySpan<byte>(new byte[] { 2 });
             int index = span.LastIndexOf(value);
@@ -103,7 +102,6 @@ namespace System.SpanTests
         [Fact]
         public static void LastIndexOfSequenceLengthOneValueAtVeryEnd_Byte()
         {
-            // A zero-length value is always "found" at the start of the span.
             ReadOnlySpan<byte> span = new ReadOnlySpan<byte>(new byte[] { 0, 1, 2, 3, 4, 5 });
             ReadOnlySpan<byte> value = new ReadOnlySpan<byte>(new byte[] { 5 });
             int index = span.LastIndexOf(value);
@@ -113,7 +111,6 @@ namespace System.SpanTests
         [Fact]
         public static void LastIndexOfSequenceLengthOneValueMultipleTimes_Byte()
         {
-            // A zero-length value is always "found" at the start of the span.
             ReadOnlySpan<byte> span = new ReadOnlySpan<byte>(new byte[] { 0, 1, 5, 3, 4, 5 });
             ReadOnlySpan<byte> value = new ReadOnlySpan<byte>(new byte[] { 5 });
             int index = span.LastIndexOf(value);
@@ -123,7 +120,6 @@ namespace System.SpanTests
         [Fact]
         public static void LastIndexOfSequenceLengthOneValueJustPasttVeryEnd_Byte()
         {
-            // A zero-length value is always "found" at the start of the span.
             ReadOnlySpan<byte> span = new ReadOnlySpan<byte>(new byte[] { 0, 1, 2, 3, 4, 5 }, 0, 5);
             ReadOnlySpan<byte> value = new ReadOnlySpan<byte>(new byte[] { 5 });
             int index = span.LastIndexOf(value);
index 5fdb3db..408c7a3 100644 (file)
@@ -93,7 +93,6 @@ namespace System.SpanTests
         [Fact]
         public static void IndexOfSequenceLengthOneValue()
         {
-            // A zero-length value is always "found" at the start of the span.
             Span<int> span = new Span<int>(new int[] { 0, 1, 2, 3, 4, 5 });
             Span<int> value = new Span<int>(new int[] { 2 });
             int index = span.IndexOf(value);
@@ -103,7 +102,6 @@ namespace System.SpanTests
         [Fact]
         public static void IndexOfSequenceLengthOneValueAtVeryEnd()
         {
-            // A zero-length value is always "found" at the start of the span.
             Span<int> span = new Span<int>(new int[] { 0, 1, 2, 3, 4, 5 });
             Span<int> value = new Span<int>(new int[] { 5 });
             int index = span.IndexOf(value);
@@ -113,7 +111,6 @@ namespace System.SpanTests
         [Fact]
         public static void IndexOfSequenceLengthOneValueJustPasttVeryEnd()
         {
-            // A zero-length value is always "found" at the start of the span.
             Span<int> span = new Span<int>(new int[] { 0, 1, 2, 3, 4, 5 }, 0, 5);
             Span<int> value = new Span<int>(new int[] { 5 });
             int index = span.IndexOf(value);
@@ -205,7 +202,6 @@ namespace System.SpanTests
         [Fact]
         public static void IndexOfSequenceLengthOneValue_String()
         {
-            // A zero-length value is always "found" at the start of the span.
             Span<string> span = new Span<string>(new string[] { "0", "1", "2", "3", "4", "5" });
             Span<string> value = new Span<string>(new string[] { "2" });
             int index = span.IndexOf(value);
@@ -215,7 +211,6 @@ namespace System.SpanTests
         [Fact]
         public static void IndexOfSequenceLengthOneValueAtVeryEnd_String()
         {
-            // A zero-length value is always "found" at the start of the span.
             Span<string> span = new Span<string>(new string[] { "0", "1", "2", "3", "4", "5" });
             Span<string> value = new Span<string>(new string[] { "5" });
             int index = span.IndexOf(value);
@@ -225,7 +220,6 @@ namespace System.SpanTests
         [Fact]
         public static void IndexOfSequenceLengthOneValueJustPasttVeryEnd_String()
         {
-            // A zero-length value is always "found" at the start of the span.
             Span<string> span = new Span<string>(new string[] { "0", "1", "2", "3", "4", "5" }, 0, 5);
             Span<string> value = new Span<string>(new string[] { "5" });
             int index = span.IndexOf(value);
index a38519b..936bfce 100644 (file)
@@ -93,7 +93,6 @@ namespace System.SpanTests
         [Fact]
         public static void IndexOfSequenceLengthOneValue_Byte()
         {
-            // A zero-length value is always "found" at the start of the span.
             Span<byte> span = new Span<byte>(new byte[] { 0, 1, 2, 3, 4, 5 });
             Span<byte> value = new Span<byte>(new byte[] { 2 });
             int index = span.IndexOf(value);
@@ -103,7 +102,6 @@ namespace System.SpanTests
         [Fact]
         public static void IndexOfSequenceLengthOneValueAtVeryEnd_Byte()
         {
-            // A zero-length value is always "found" at the start of the span.
             Span<byte> span = new Span<byte>(new byte[] { 0, 1, 2, 3, 4, 5 });
             Span<byte> value = new Span<byte>(new byte[] { 5 });
             int index = span.IndexOf(value);
@@ -113,7 +111,6 @@ namespace System.SpanTests
         [Fact]
         public static void IndexOfSequenceLengthOneValueJustPasttVeryEnd_Byte()
         {
-            // A zero-length value is always "found" at the start of the span.
             Span<byte> span = new Span<byte>(new byte[] { 0, 1, 2, 3, 4, 5 }, 0, 5);
             Span<byte> value = new Span<byte>(new byte[] { 5 });
             int index = span.IndexOf(value);
index fc69cdd..bd6452e 100644 (file)
@@ -93,7 +93,6 @@ namespace System.SpanTests
         [Fact]
         public static void IndexOfSequenceLengthOneValue_Char()
         {
-            // A zero-length value is always "found" at the start of the span.
             Span<char> span = new Span<char>(new char[] { '0', '1', '2', '3', '4', '5' });
             Span<char> value = new Span<char>(new char[] { '2' });
             int index = span.IndexOf(value);
@@ -103,7 +102,6 @@ namespace System.SpanTests
         [Fact]
         public static void IndexOfSequenceLengthOneValueAtVeryEnd_Char()
         {
-            // A zero-length value is always "found" at the start of the span.
             Span<char> span = new Span<char>(new char[] { '0', '1', '2', '3', '4', '5' });
             Span<char> value = new Span<char>(new char[] { '5' });
             int index = span.IndexOf(value);
@@ -113,7 +111,6 @@ namespace System.SpanTests
         [Fact]
         public static void IndexOfSequenceLengthOneValueJustPasttVeryEnd_Char()
         {
-            // A zero-length value is always "found" at the start of the span.
             Span<char> span = new Span<char>(new char[] { '0', '1', '2', '3', '4', '5' }, 0, 5);
             Span<char> value = new Span<char>(new char[] { '5' });
             int index = span.IndexOf(value);
index d130e15..a7bf8cd 100644 (file)
@@ -74,11 +74,11 @@ namespace System.SpanTests
         [Fact]
         public static void LastIndexOfSequenceZeroLengthValue()
         {
-            // A zero-length value is always "found" at the start of the span.
+            // A zero-length value is always "found" at the end of the span.
             Span<int> span = new Span<int>(new int[] { 0, 1, 77, 2, 3, 77, 77, 4, 5, 77, 77, 77, 88, 6, 6, 77, 77, 88, 9 });
             Span<int> value = new Span<int>(Array.Empty<int>());
             int index = span.LastIndexOf(value);
-            Assert.Equal(0, index);
+            Assert.Equal(span.Length, index);
         }
 
         [Fact]
@@ -93,7 +93,6 @@ namespace System.SpanTests
         [Fact]
         public static void LastIndexOfSequenceLengthOneValue()
         {
-            // A zero-length value is always "found" at the start of the span.
             Span<int> span = new Span<int>(new int[] { 0, 1, 2, 3, 4, 5 });
             Span<int> value = new Span<int>(new int[] { 2 });
             int index = span.LastIndexOf(value);
@@ -103,7 +102,6 @@ namespace System.SpanTests
         [Fact]
         public static void LastIndexOfSequenceLengthOneValueAtVeryEnd()
         {
-            // A zero-length value is always "found" at the start of the span.
             Span<int> span = new Span<int>(new int[] { 0, 1, 2, 3, 4, 5 });
             Span<int> value = new Span<int>(new int[] { 5 });
             int index = span.LastIndexOf(value);
@@ -113,7 +111,6 @@ namespace System.SpanTests
         [Fact]
         public static void LastIndexOfSequenceLengthOneValueMultipleTimes()
         {
-            // A zero-length value is always "found" at the start of the span.
             Span<int> span = new Span<int>(new int[] { 0, 1, 5, 3, 4, 5 });
             Span<int> value = new Span<int>(new int[] { 5 });
             int index = span.LastIndexOf(value);
@@ -123,7 +120,6 @@ namespace System.SpanTests
         [Fact]
         public static void LastIndexOfSequenceLengthOneValueJustPasttVeryEnd()
         {
-            // A zero-length value is always "found" at the start of the span.
             Span<int> span = new Span<int>(new int[] { 0, 1, 2, 3, 4, 5 }, 0, 5);
             Span<int> value = new Span<int>(new int[] { 5 });
             int index = span.LastIndexOf(value);
@@ -196,11 +192,11 @@ namespace System.SpanTests
         [Fact]
         public static void LastIndexOfSequenceZeroLengthValue_String()
         {
-            // A zero-length value is always "found" at the start of the span.
+            // A zero-length value is always "found" at the end of the span.
             Span<string> span = new Span<string>(new string[] { "0", "1", "77", "2", "3", "77", "77", "4", "5", "77", "77", "77", "88", "6", "6", "77", "77", "88", "9" });
             Span<string> value = new Span<string>(Array.Empty<string>());
             int index = span.LastIndexOf(value);
-            Assert.Equal(0, index);
+            Assert.Equal(span.Length, index);
         }
 
         [Fact]
@@ -215,7 +211,6 @@ namespace System.SpanTests
         [Fact]
         public static void LastIndexOfSequenceLengthOneValue_String()
         {
-            // A zero-length value is always "found" at the start of the span.
             Span<string> span = new Span<string>(new string[] { "0", "1", "2", "3", "4", "5" });
             Span<string> value = new Span<string>(new string[] { "2" });
             int index = span.LastIndexOf(value);
@@ -225,7 +220,6 @@ namespace System.SpanTests
         [Fact]
         public static void LastIndexOfSequenceLengthOneValueAtVeryEnd_String()
         {
-            // A zero-length value is always "found" at the start of the span.
             Span<string> span = new Span<string>(new string[] { "0", "1", "2", "3", "4", "5" });
             Span<string> value = new Span<string>(new string[] { "5" });
             int index = span.LastIndexOf(value);
@@ -235,7 +229,6 @@ namespace System.SpanTests
         [Fact]
         public static void LastIndexOfSequenceLengthOneValueJustPasttVeryEnd_String()
         {
-            // A zero-length value is always "found" at the start of the span.
             Span<string> span = new Span<string>(new string[] { "0", "1", "2", "3", "4", "5" }, 0, 5);
             Span<string> value = new Span<string>(new string[] { "5" });
             int index = span.LastIndexOf(value);
index 96b2297..8c8a02d 100644 (file)
@@ -74,11 +74,11 @@ namespace System.SpanTests
         [Fact]
         public static void LastIndexOfSequenceZeroLengthValue_Byte()
         {
-            // A zero-length value is always "found" at the start of the span.
+            // A zero-length value is always "found" at the end of the span.
             Span<byte> span = new Span<byte>(new byte[] { 0, 1, 77, 2, 3, 77, 77, 4, 5, 77, 77, 77, 88, 6, 6, 77, 77, 88, 9 });
             Span<byte> value = new Span<byte>(Array.Empty<byte>());
             int index = span.LastIndexOf(value);
-            Assert.Equal(0, index);
+            Assert.Equal(span.Length, index);
         }
 
         [Fact]
@@ -93,7 +93,6 @@ namespace System.SpanTests
         [Fact]
         public static void LastIndexOfSequenceLengthOneValue_Byte()
         {
-            // A zero-length value is always "found" at the start of the span.
             Span<byte> span = new Span<byte>(new byte[] { 0, 1, 2, 3, 4, 5 });
             Span<byte> value = new Span<byte>(new byte[] { 2 });
             int index = span.LastIndexOf(value);
@@ -103,7 +102,6 @@ namespace System.SpanTests
         [Fact]
         public static void LastIndexOfSequenceLengthOneValueAtVeryEnd_Byte()
         {
-            // A zero-length value is always "found" at the start of the span.
             Span<byte> span = new Span<byte>(new byte[] { 0, 1, 2, 3, 4, 5 });
             Span<byte> value = new Span<byte>(new byte[] { 5 });
             int index = span.LastIndexOf(value);
@@ -113,7 +111,6 @@ namespace System.SpanTests
         [Fact]
         public static void LastIndexOfSequenceLengthOneValueMultipleTimes_Byte()
         {
-            // A zero-length value is always "found" at the start of the span.
             Span<byte> span = new Span<byte>(new byte[] { 0, 1, 5, 3, 4, 5 });
             Span<byte> value = new Span<byte>(new byte[] { 5 });
             int index = span.LastIndexOf(value);
@@ -123,7 +120,6 @@ namespace System.SpanTests
         [Fact]
         public static void LastIndexOfSequenceLengthOneValueJustPasttVeryEnd_Byte()
         {
-            // A zero-length value is always "found" at the start of the span.
             Span<byte> span = new Span<byte>(new byte[] { 0, 1, 2, 3, 4, 5 }, 0, 5);
             Span<byte> value = new Span<byte>(new byte[] { 5 });
             int index = span.LastIndexOf(value);
index af027cb..362257f 100644 (file)
@@ -74,11 +74,11 @@ namespace System.SpanTests
         [Fact]
         public static void LastIndexOfSequenceZeroLengthValue_Char()
         {
-            // A zero-length value is always "found" at the start of the span.
+            // A zero-length value is always "found" at the end of the span.
             Span<char> span = new Span<char>(new char[] { '0', '1', '7', '2', '3', '7', '7', '4', '5', '7', '7', '7', '8', '6', '6', '7', '7', '8', '9' });
             Span<char> value = new Span<char>(Array.Empty<char>());
             int index = span.LastIndexOf(value);
-            Assert.Equal(0, index);
+            Assert.Equal(span.Length, index);
         }
 
         [Fact]
@@ -93,7 +93,6 @@ namespace System.SpanTests
         [Fact]
         public static void LastIndexOfSequenceLengthOneValue_Char()
         {
-            // A zero-length value is always "found" at the start of the span.
             Span<char> span = new Span<char>(new char[] { '0', '1', '2', '3', '4', '5' });
             Span<char> value = new Span<char>(new char[] { '2' });
             int index = span.LastIndexOf(value);
@@ -103,7 +102,6 @@ namespace System.SpanTests
         [Fact]
         public static void LastIndexOfSequenceLengthOneValueAtVeryEnd_Char()
         {
-            // A zero-length value is always "found" at the start of the span.
             Span<char> span = new Span<char>(new char[] { '0', '1', '2', '3', '4', '5' });
             Span<char> value = new Span<char>(new char[] { '5' });
             int index = span.LastIndexOf(value);
@@ -113,7 +111,6 @@ namespace System.SpanTests
         [Fact]
         public static void LastIndexOfSequenceLengthOneValueMultipleTimes_Char()
         {
-            // A zero-length value is always "found" at the start of the span.
             Span<char> span = new Span<char>(new char[] { '0', '1', '5', '3', '4', '5' });
             Span<char> value = new Span<char>(new char[] { '5' });
             int index = span.LastIndexOf(value);
@@ -123,7 +120,6 @@ namespace System.SpanTests
         [Fact]
         public static void LastIndexOfSequenceLengthOneValueJustPasttVeryEnd_Char()
         {
-            // A zero-length value is always "found" at the start of the span.
             Span<char> span = new Span<char>(new char[] { '0', '1', '2', '3', '4', '5' }, 0, 5);
             Span<char> value = new Span<char>(new char[] { '5' });
             int index = span.LastIndexOf(value);
index 16b596e..f468c54 100644 (file)
@@ -505,12 +505,12 @@ namespace System
         {
             { new string[] { "1", null, "2" }, new string[] { "1", null, "2" }, 0},
             { new string[] { "1", null, "2" }, new string[] { null }, 1},
-            { new string[] { "1", null, "2" }, (string[])null, 0},
+            { new string[] { "1", null, "2" }, (string[])null, 3},
 
             { new string[] { "1", "3", "1" }, new string[] { "1", null, "2" }, -1},
             { new string[] { "1", "3", "1" }, new string[] { "1" }, 2},
             { new string[] { "1", "3", "1" }, new string[] { null }, -1},
-            { new string[] { "1", "3", "1" }, (string[])null, 0},
+            { new string[] { "1", "3", "1" }, (string[])null, 3},
 
             { null, new string[] { "1", null, "2" }, -1},
 
index 663530b..922a471 100644 (file)
@@ -40,9 +40,9 @@ namespace System.Globalization
             }
         }
 
-        internal static unsafe int InvariantLastIndexOf(string source, string value, int startIndex, int count, bool ignoreCase)
+        private static unsafe int InvariantLastIndexOf(string source, string value, int startIndex, int count, bool ignoreCase)
         {
-            Debug.Assert(source != null);
+            Debug.Assert(!string.IsNullOrEmpty(source));
             Debug.Assert(value != null);
             Debug.Assert(startIndex >= 0 && startIndex < source.Length);
 
@@ -73,7 +73,7 @@ namespace System.Globalization
 
             if (valueCount == 0)
             {
-                return fromBeginning ? 0 : sourceCount - 1;
+                return fromBeginning ? 0 : sourceCount;
             }
 
             if (sourceCount < valueCount)
index 4158890..d0db147 100644 (file)
@@ -465,9 +465,12 @@ namespace System.Globalization
             Debug.Assert(target != null);
             Debug.Assert((options & CompareOptions.OrdinalIgnoreCase) == 0);
 
+            // startIndex points to the final char to include in the search space.
+            // empty target strings trivially occur at the end of the search space.
+
             if (target.Length == 0)
             {
-                return startIndex;
+                return startIndex + 1;
             }
 
             if (options == CompareOptions.Ordinal)
index 897cb5b..cbc7ac0 100644 (file)
@@ -369,11 +369,15 @@ namespace System.Globalization
         {
             Debug.Assert(!GlobalizationMode.Invariant);
 
+            Debug.Assert(!string.IsNullOrEmpty(source));
             Debug.Assert(target != null);
             Debug.Assert((options & CompareOptions.OrdinalIgnoreCase) == 0);
 
+            // startIndex points to the final char to include in the search space.
+            // empty target strings trivially occur at the end of the search space.
+
             if (target.Length == 0)
-                return startIndex;
+                return startIndex + 1;
 
             if ((options & CompareOptions.Ordinal) != 0)
             {
index e0b4da0..57c6f81 100644 (file)
@@ -1336,10 +1336,10 @@ namespace System.Globalization
                     count--;
                 }
 
-                // If we are looking for nothing, just return 0
+                // empty substrings trivially occur at the end of the search space
                 if (value.Length == 0 && count >= 0 && startIndex - count + 1 >= 0)
                 {
-                    return startIndex;
+                    return startIndex + 1;
                 }
             }
 
@@ -1360,9 +1360,9 @@ namespace System.Globalization
             return LastIndexOfCore(source, value, startIndex, count, options);
         }
 
-        internal static int LastIndexOfOrdinal(string source, string value, int startIndex, int count, bool ignoreCase)
+        private static int LastIndexOfOrdinal(string source, string value, int startIndex, int count, bool ignoreCase)
         {
-            Debug.Assert(source != null);
+            Debug.Assert(!string.IsNullOrEmpty(source));
             Debug.Assert(value != null);
 
             if (GlobalizationMode.Invariant)
@@ -1377,7 +1377,7 @@ namespace System.Globalization
 
             if (value.Length == 0)
             {
-                return Math.Max(0, startIndex - count + 1);
+                return startIndex + 1; // startIndex is the index of the last char to include in the search space
             }
 
             if (count == 0)
index 604035e..fb339f7 100644 (file)
@@ -142,7 +142,7 @@ namespace System
 
             if (value.Length == 0)
             {
-                return 0;
+                return 0; // empty substring trivially occurs at every index (including start) of search space
             }
 
             if (span.Length == 0)
@@ -192,7 +192,7 @@ namespace System
 
             if (value.Length == 0)
             {
-                return span.Length > 0 ? span.Length - 1 : 0;
+                return span.Length; // empty substring trivially occurs at every index (including end) of search space
             }
 
             if (span.Length == 0)
@@ -337,7 +337,7 @@ namespace System
 
             if (value.Length == 0)
             {
-                return true;
+                return true; // the empty string is trivially a suffix of every other string
             }
 
             if (comparisonType >= StringComparison.Ordinal || GlobalizationMode.Invariant)
@@ -370,7 +370,7 @@ namespace System
 
             if (value.Length == 0)
             {
-                return true;
+                return true; // the empty string is trivially a prefix of every other string
             }
 
             if (comparisonType >= StringComparison.Ordinal || GlobalizationMode.Invariant)
index 4d84fd3..025d22e 100644 (file)
@@ -430,7 +430,7 @@ namespace System
             Debug.Assert(valueLength >= 0);
 
             if (valueLength == 0)
-                return 0;  // A zero-length sequence is always treated as "found" at the start of the search space.
+                return searchSpaceLength;  // A zero-length sequence is always treated as "found" at the end of the search space.
 
             byte valueHead = value;
             ref byte valueTail = ref Unsafe.Add(ref value, 1);
index 0b354c3..4177c45 100644 (file)
@@ -441,7 +441,7 @@ namespace System
             Debug.Assert(valueLength >= 0);
 
             if (valueLength == 0)
-                return 0;  // A zero-length sequence is always treated as "found" at the start of the search space.
+                return searchSpaceLength;  // A zero-length sequence is always treated as "found" at the end of the search space.
 
             T valueHead = value;
             ref T valueTail = ref Unsafe.Add(ref value, 1);
index b664044..5ceed67 100644 (file)
@@ -929,5 +929,16 @@ namespace System
 
             return (CompareOptions)((int)comparisonType & (int)CompareOptions.IgnoreCase);
         }
+
+        private static CompareOptions GetCompareOptionsFromOrdinalStringComparison(StringComparison comparisonType)
+        {
+            Debug.Assert(comparisonType == StringComparison.Ordinal || comparisonType == StringComparison.OrdinalIgnoreCase);
+
+            // StringComparison.Ordinal (0x04) --> CompareOptions.Ordinal (0x4000_0000)
+            // StringComparison.OrdinalIgnoreCase (0x05) -> CompareOptions.OrdinalIgnoreCase (0x1000_0000)
+
+            int ct = (int)comparisonType;
+            return (CompareOptions)((ct & -ct) << 28); // neg and shl
+        }
     }
 }
index e80cd43..93de7fa 100644 (file)
@@ -219,6 +219,85 @@ namespace System
             charMap[value & PROBABILISTICMAP_BLOCK_INDEX_MASK] |= 1u << (value >> PROBABILISTICMAP_BLOCK_INDEX_SHIFT);
         }
 
+       /*
+        * IndexOf, LastIndexOf, Contains, StartsWith, and EndsWith
+        * ========================================================
+        *
+        * Given a search string 'searchString', a target string 'value' to locate within the search string, and a comparer
+        * 'comparer', the comparer will return a set S of tuples '(startPos, endPos)' for which the below expression
+        * returns true:
+        *
+        * >> bool result = searchString.Substring(startPos, endPos - startPos).Equals(value, comparer);
+        *
+        * If the set S is empty (i.e., there is no combination of values 'startPos' and 'endPos' which makes the
+        * above expression evaluate to true), then we say "'searchString' does not contain 'value'", and the expression
+        * "searchString.Contains(value, comparer)" should evaluate to false. If the set S is non-empty, then we say
+        * "'searchString' contains 'value'", and the expression "searchString.Contains(value, comparer)" should
+        * evaluate to true.
+        *
+        * Given a 'searchString', 'value', and 'comparer', the behavior of the IndexOf method is that it finds the
+        * smallest possible 'endPos' for which there exists any corresponding 'startPos' which makes the above
+        * expression evaluate to true, then it returns any 'startPos' within that subset. For example:
+        *
+        * let searchString = "<ZWJ><ZWJ>hihi" (where <ZWJ> = U+200D ZERO WIDTH JOINER, a weightless code point)
+        * let value = "hi"
+        * let comparer = a linguistic culture-invariant comparer (e.g., StringComparison.InvariantCulture)
+        * then S = { (0, 4), (1, 4), (2, 4), (4, 6) }
+        * so the expression "<ZWJ><ZWJ>hihi".IndexOf("hi", comparer) can evaluate to any of { 0, 1, 2 }.
+        *
+        * n.b. ordinal comparers (e.g., StringComparison.Ordinal and StringComparison.OrdinalIgnoreCase) do not
+        * exhibit this ambiguity, as any given 'startPos' or 'endPos' will appear at most exactly once across
+        * all entries from set S. With the above example, S = { (2, 4), (4, 6) }, so IndexOf = 2 unambiguously.
+        *
+        * There exists a relationship between IndexOf and StartsWith. If there exists in set S any entry with
+        * the tuple values (startPos = 0, endPos = <anything>), we say "'searchString' starts with 'value'", and
+        * the expression "searchString.StartsWith(value, comparer)" should evaluate to true. If there exists
+        * no such entry in set S, then we say "'searchString' does not start with 'value'", and the expression
+        * "searchString.StartsWith(value, comparer)" should evaluate to false.
+        *
+        * LastIndexOf and EndsWith have a similar relationship as IndexOf and StartsWith. The behavior of the
+        * LastIndexOf method is that it finds the largest possible 'endPos' for which there exists any corresponding
+        * 'startPos' which makes the expression evaluate to true, then it returns any 'startPos' within that
+        * subset. For example:
+        *
+        * let searchString = "hi<ZWJ><ZWJ>hi" (this is slightly modified from the earlier example)
+        * let value = "hi"
+        * let comparer = StringComparison.InvariantCulture
+        * then S = { (0, 2), (0, 3), (0, 4), (2, 6), (3, 6), (4, 6) }
+        * so the expression "hi<ZWJ><ZWJ>hi".LastIndexOf("hi", comparer) can evaluate to any of { 2, 3, 4 }.
+        *
+        * If there exists in set S any entry with the tuple values (startPos = <anything>, endPos = searchString.Length),
+        * we say "'searchString' ends with 'value'", and the expression "searchString.EndsWith(value, comparer)"
+        * should evaluate to true. If there exists no such entry in set S, then we say "'searchString' does not
+        * start with 'value'", and the expression "searchString.EndsWith(value, comparer)" should evaluate to false.
+        *
+        * There are overloads of IndexOf and LastIndexOf which take an offset and length in order to constrain the
+        * search space to a substring of the original search string.
+        *
+        * For LastIndexOf specifially, overloads which take a 'startIndex' and 'count' behave differently
+        * than their IndexOf counterparts. 'startIndex' is the index of the last char element that should
+        * be considered when performing the search. For example, if startIndex = 4, then the caller is
+        * indicating "when finding the match I want you to include the char element at index 4, but not
+        * any char elements past that point."
+        *
+        *                        idx = 0123456 ("abcdefg".Length = 7)
+        * So, if the search string is "abcdefg", startIndex = 5 and count = 3, then the search space will
+        *                                 ~~~    be the substring "def", as highlighted to the left.
+        * Essentially: "the search space should be of length 3 chars and should end *just after* the char
+        * element at index 5."
+        *
+        * Since this behavior can introduce off-by-one errors in the boundary cases, we allow startIndex = -1
+        * with a zero-length 'searchString' (treated as equivalent to startIndex = 0), and we allow
+        * startIndex = searchString.Length (treated as equivalent to startIndex = searchString.Length - 1).
+        *
+        * Note also that this behavior can introduce errors when dealing with UTF-16 surrogate pairs.
+        * If the search string is the 3 chars "[BMP][HI][LO]", startIndex = 1 and count = 2, then the
+        *                                      ~~~~~~~~~       search space wil be the substring "[BMP][ HI]".
+        * This means that the char [HI] is incorrectly seen as a standalone high surrogate, which could
+        * lead to incorrect matching behavior, or it could cause LastIndexOf to incorrectly report that
+        * a zero-weight character could appear between the [HI] and [LO] chars.
+        */
+
         public int IndexOf(string value)
         {
             return IndexOf(value, StringComparison.CurrentCulture);
@@ -439,32 +518,7 @@ namespace System
 
         public int LastIndexOf(string value, int startIndex, int count, StringComparison comparisonType)
         {
-            if (value == null)
-                throw new ArgumentNullException(nameof(value));
-
-            // Special case for 0 length input strings
-            if (this.Length == 0 && (startIndex == -1 || startIndex == 0))
-                return (value.Length == 0) ? 0 : -1;
-
-            // Now after handling empty strings, make sure we're not out of range
-            if (startIndex < 0 || startIndex > this.Length)
-                throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_Index);
-
-            // Make sure that we allow startIndex == this.Length
-            if (startIndex == this.Length)
-            {
-                startIndex--;
-                if (count > 0)
-                    count--;
-            }
-
-            // 2nd half of this also catches when startIndex == MAXINT, so MAXINT - 0 + 1 == -1, which is < 0.
-            if (count < 0 || startIndex - count + 1 < 0)
-                throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_Count);
-
-            // If we are looking for nothing, just return startIndex
-            if (value.Length == 0)
-                return startIndex;
+            // Parameter checking will be done by CompareInfo.LastIndexOf.
 
             switch (comparisonType)
             {
@@ -478,10 +532,12 @@ namespace System
 
                 case StringComparison.Ordinal:
                 case StringComparison.OrdinalIgnoreCase:
-                    return CompareInfo.LastIndexOfOrdinal(this, value, startIndex, count, GetCaseCompareOfComparisonCulture(comparisonType) != CompareOptions.None);
+                    return CompareInfo.Invariant.LastIndexOf(this, value, startIndex, count, GetCompareOptionsFromOrdinalStringComparison(comparisonType));
 
                 default:
-                    throw new ArgumentException(SR.NotSupported_StringComparison, nameof(comparisonType));
+                    throw (value is null)
+                        ? new ArgumentNullException(nameof(value))
+                        : new ArgumentException(SR.NotSupported_StringComparison, nameof(comparisonType));
             }
         }