From: Levi Broderick Date: Tue, 7 Apr 2020 19:16:24 +0000 (-0700) Subject: Fix various LastIndexOf bugs when given zero-length target values (#34616) X-Git-Tag: submit/tizen/20210909.063632~8712 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=c4507d795038c41fc041276e0737045262f9980d;p=platform%2Fupstream%2Fdotnet%2Fruntime.git Fix various LastIndexOf bugs when given zero-length target values (#34616) - string.LastIndexOf(string.Empty) shouldn't perform -1 adjustment - MemoryExtensions.LastIndexOf(ROS, string.Empty) shouldn't return 0 - Tighten up some existing unit tests --- diff --git a/src/libraries/Common/tests/Tests/System/StringTests.cs b/src/libraries/Common/tests/Tests/System/StringTests.cs index fd24ad5..ba1757f 100644 --- a/src/libraries/Common/tests/Tests/System/StringTests.cs +++ b/src/libraries/Common/tests/Tests/System/StringTests.cs @@ -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 span = s1.AsSpan(); ReadOnlySpan 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 span = s1.AsSpan(); ReadOnlySpan 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 span = s1.AsSpan(); ReadOnlySpan 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 span = s1.AsSpan(); ReadOnlySpan 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 span = s1.AsSpan(); ReadOnlySpan 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 span = s1.AsSpan(); ReadOnlySpan 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 span = s1.AsSpan(); ReadOnlySpan 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 span = s1.AsSpan(); ReadOnlySpan value = s2.AsSpan(); index = span.LastIndexOf(value); diff --git a/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.LastIndexOf.cs b/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.LastIndexOf.cs index 459f0b7..4d0474e 100644 --- a/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.LastIndexOf.cs +++ b/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.LastIndexOf.cs @@ -17,7 +17,7 @@ namespace System.Globalization.Tests public static IEnumerable 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 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, ReadOnlySpan, 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)); } } diff --git a/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.cs b/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.cs index d319c69..9967bd4 100644 --- a/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.cs +++ b/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.cs @@ -310,7 +310,7 @@ namespace System.Globalization.Tests public static IEnumerable 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 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] diff --git a/src/libraries/System.Globalization/tests/Invariant/InvariantMode.cs b/src/libraries/System.Globalization/tests/Invariant/InvariantMode.cs index 5291add..d6c7676 100644 --- a/src/libraries/System.Globalization/tests/Invariant/InvariantMode.cs +++ b/src/libraries/System.Globalization/tests/Invariant/InvariantMode.cs @@ -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 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 sourceBoundedMemory = BoundedMemory.AllocateFromExistingData(source); + sourceBoundedMemory.MakeReadonly(); + ReadOnlySpan sourceBoundedSpan = sourceBoundedMemory.Span; + + using BoundedMemory valueBoundedMemory = BoundedMemory.AllocateFromExistingData(value); + valueBoundedMemory.MakeReadonly(); + ReadOnlySpan 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 sourceBoundedMemory = BoundedMemory.AllocateFromExistingData(source); + sourceBoundedMemory.MakeReadonly(); + ReadOnlySpan sourceBoundedSpan = sourceBoundedMemory.Span; + + using BoundedMemory valueBoundedMemory = BoundedMemory.AllocateFromExistingData(value); + valueBoundedMemory.MakeReadonly(); + ReadOnlySpan 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 sourceBoundedMemory = BoundedMemory.AllocateFromExistingData(source); + sourceBoundedMemory.MakeReadonly(); + ReadOnlySpan sourceBoundedSpan = sourceBoundedMemory.Span; + + using BoundedMemory valueBoundedMemory = BoundedMemory.AllocateFromExistingData(value); + valueBoundedMemory.MakeReadonly(); + ReadOnlySpan valueBoundedSpan = valueBoundedMemory.Span; + + res = sourceBoundedSpan.CompareTo(valueBoundedSpan, GetStringComparison(options)); + Assert.Equal(result, Math.Sign(res)); } } diff --git a/src/libraries/System.Memory/tests/ReadOnlySpan/IndexOfSequence.T.cs b/src/libraries/System.Memory/tests/ReadOnlySpan/IndexOfSequence.T.cs index cb0fd3a..85d73a8 100644 --- a/src/libraries/System.Memory/tests/ReadOnlySpan/IndexOfSequence.T.cs +++ b/src/libraries/System.Memory/tests/ReadOnlySpan/IndexOfSequence.T.cs @@ -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 span = new ReadOnlySpan(new int[] { 0, 1, 2, 3, 4, 5 }); ReadOnlySpan value = new ReadOnlySpan(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 span = new ReadOnlySpan(new int[] { 0, 1, 2, 3, 4, 5 }); ReadOnlySpan value = new ReadOnlySpan(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 span = new ReadOnlySpan(new int[] { 0, 1, 2, 3, 4, 5 }, 0, 5); ReadOnlySpan value = new ReadOnlySpan(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 span = new ReadOnlySpan(new string[] { "0", "1", "2", "3", "4", "5" }); ReadOnlySpan value = new ReadOnlySpan(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 span = new ReadOnlySpan(new string[] { "0", "1", "2", "3", "4", "5" }); ReadOnlySpan value = new ReadOnlySpan(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 span = new ReadOnlySpan(new string[] { "0", "1", "2", "3", "4", "5" }, 0, 5); ReadOnlySpan value = new ReadOnlySpan(new string[] { "5" }); int index = span.IndexOf(value); diff --git a/src/libraries/System.Memory/tests/ReadOnlySpan/IndexOfSequence.byte.cs b/src/libraries/System.Memory/tests/ReadOnlySpan/IndexOfSequence.byte.cs index 3a23695..c823d97 100644 --- a/src/libraries/System.Memory/tests/ReadOnlySpan/IndexOfSequence.byte.cs +++ b/src/libraries/System.Memory/tests/ReadOnlySpan/IndexOfSequence.byte.cs @@ -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 span = new ReadOnlySpan(new byte[] { 0, 1, 2, 3, 4, 5 }); ReadOnlySpan value = new ReadOnlySpan(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 span = new ReadOnlySpan(new byte[] { 0, 1, 2, 3, 4, 5 }); ReadOnlySpan value = new ReadOnlySpan(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 span = new ReadOnlySpan(new byte[] { 0, 1, 2, 3, 4, 5 }, 0, 5); ReadOnlySpan value = new ReadOnlySpan(new byte[] { 5 }); int index = span.IndexOf(value); diff --git a/src/libraries/System.Memory/tests/ReadOnlySpan/LastIndexOfSequence.T.cs b/src/libraries/System.Memory/tests/ReadOnlySpan/LastIndexOfSequence.T.cs index 667d2b7..22556e3 100644 --- a/src/libraries/System.Memory/tests/ReadOnlySpan/LastIndexOfSequence.T.cs +++ b/src/libraries/System.Memory/tests/ReadOnlySpan/LastIndexOfSequence.T.cs @@ -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 span = new ReadOnlySpan(new int[] { 0, 1, 77, 2, 3, 77, 77, 4, 5, 77, 77, 77, 88, 6, 6, 77, 77, 88, 9 }); ReadOnlySpan value = new ReadOnlySpan(Array.Empty()); 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 span = new ReadOnlySpan(new int[] { 0, 1, 2, 3, 4, 5 }); ReadOnlySpan value = new ReadOnlySpan(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 span = new ReadOnlySpan(new int[] { 0, 1, 2, 3, 4, 5 }); ReadOnlySpan value = new ReadOnlySpan(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 span = new ReadOnlySpan(new int[] { 0, 1, 5, 3, 4, 5 }); ReadOnlySpan value = new ReadOnlySpan(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 span = new ReadOnlySpan(new int[] { 0, 1, 2, 3, 4, 5 }, 0, 5); ReadOnlySpan value = new ReadOnlySpan(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 span = new ReadOnlySpan(new string[] { "0", "1", "77", "2", "3", "77", "77", "4", "5", "77", "77", "77", "88", "6", "6", "77", "77", "88", "9" }); ReadOnlySpan value = new ReadOnlySpan(Array.Empty()); 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 span = new ReadOnlySpan(new string[] { "0", "1", "2", "3", "4", "5" }); ReadOnlySpan value = new ReadOnlySpan(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 span = new ReadOnlySpan(new string[] { "0", "1", "2", "3", "4", "5" }); ReadOnlySpan value = new ReadOnlySpan(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 span = new ReadOnlySpan(new string[] { "0", "1", "2", "3", "4", "5" }, 0, 5); ReadOnlySpan value = new ReadOnlySpan(new string[] { "5" }); int index = span.LastIndexOf(value); diff --git a/src/libraries/System.Memory/tests/ReadOnlySpan/LastIndexOfSequence.byte.cs b/src/libraries/System.Memory/tests/ReadOnlySpan/LastIndexOfSequence.byte.cs index 293920a..1e36c1c 100644 --- a/src/libraries/System.Memory/tests/ReadOnlySpan/LastIndexOfSequence.byte.cs +++ b/src/libraries/System.Memory/tests/ReadOnlySpan/LastIndexOfSequence.byte.cs @@ -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 span = new ReadOnlySpan(new byte[] { 0, 1, 77, 2, 3, 77, 77, 4, 5, 77, 77, 77, 88, 6, 6, 77, 77, 88, 9 }); ReadOnlySpan value = new ReadOnlySpan(Array.Empty()); 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 span = new ReadOnlySpan(new byte[] { 0, 1, 2, 3, 4, 5 }); ReadOnlySpan value = new ReadOnlySpan(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 span = new ReadOnlySpan(new byte[] { 0, 1, 2, 3, 4, 5 }); ReadOnlySpan value = new ReadOnlySpan(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 span = new ReadOnlySpan(new byte[] { 0, 1, 5, 3, 4, 5 }); ReadOnlySpan value = new ReadOnlySpan(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 span = new ReadOnlySpan(new byte[] { 0, 1, 2, 3, 4, 5 }, 0, 5); ReadOnlySpan value = new ReadOnlySpan(new byte[] { 5 }); int index = span.LastIndexOf(value); diff --git a/src/libraries/System.Memory/tests/Span/IndexOfSequence.T.cs b/src/libraries/System.Memory/tests/Span/IndexOfSequence.T.cs index 5fdb3db..408c7a3 100644 --- a/src/libraries/System.Memory/tests/Span/IndexOfSequence.T.cs +++ b/src/libraries/System.Memory/tests/Span/IndexOfSequence.T.cs @@ -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 span = new Span(new int[] { 0, 1, 2, 3, 4, 5 }); Span value = new Span(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 span = new Span(new int[] { 0, 1, 2, 3, 4, 5 }); Span value = new Span(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 span = new Span(new int[] { 0, 1, 2, 3, 4, 5 }, 0, 5); Span value = new Span(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 span = new Span(new string[] { "0", "1", "2", "3", "4", "5" }); Span value = new Span(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 span = new Span(new string[] { "0", "1", "2", "3", "4", "5" }); Span value = new Span(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 span = new Span(new string[] { "0", "1", "2", "3", "4", "5" }, 0, 5); Span value = new Span(new string[] { "5" }); int index = span.IndexOf(value); diff --git a/src/libraries/System.Memory/tests/Span/IndexOfSequence.byte.cs b/src/libraries/System.Memory/tests/Span/IndexOfSequence.byte.cs index a38519b..936bfce 100644 --- a/src/libraries/System.Memory/tests/Span/IndexOfSequence.byte.cs +++ b/src/libraries/System.Memory/tests/Span/IndexOfSequence.byte.cs @@ -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 span = new Span(new byte[] { 0, 1, 2, 3, 4, 5 }); Span value = new Span(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 span = new Span(new byte[] { 0, 1, 2, 3, 4, 5 }); Span value = new Span(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 span = new Span(new byte[] { 0, 1, 2, 3, 4, 5 }, 0, 5); Span value = new Span(new byte[] { 5 }); int index = span.IndexOf(value); diff --git a/src/libraries/System.Memory/tests/Span/IndexOfSequence.char.cs b/src/libraries/System.Memory/tests/Span/IndexOfSequence.char.cs index fc69cdd..bd6452e 100644 --- a/src/libraries/System.Memory/tests/Span/IndexOfSequence.char.cs +++ b/src/libraries/System.Memory/tests/Span/IndexOfSequence.char.cs @@ -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 span = new Span(new char[] { '0', '1', '2', '3', '4', '5' }); Span value = new Span(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 span = new Span(new char[] { '0', '1', '2', '3', '4', '5' }); Span value = new Span(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 span = new Span(new char[] { '0', '1', '2', '3', '4', '5' }, 0, 5); Span value = new Span(new char[] { '5' }); int index = span.IndexOf(value); diff --git a/src/libraries/System.Memory/tests/Span/LastIndexOfSequence.T.cs b/src/libraries/System.Memory/tests/Span/LastIndexOfSequence.T.cs index d130e15..a7bf8cd4 100644 --- a/src/libraries/System.Memory/tests/Span/LastIndexOfSequence.T.cs +++ b/src/libraries/System.Memory/tests/Span/LastIndexOfSequence.T.cs @@ -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 span = new Span(new int[] { 0, 1, 77, 2, 3, 77, 77, 4, 5, 77, 77, 77, 88, 6, 6, 77, 77, 88, 9 }); Span value = new Span(Array.Empty()); 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 span = new Span(new int[] { 0, 1, 2, 3, 4, 5 }); Span value = new Span(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 span = new Span(new int[] { 0, 1, 2, 3, 4, 5 }); Span value = new Span(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 span = new Span(new int[] { 0, 1, 5, 3, 4, 5 }); Span value = new Span(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 span = new Span(new int[] { 0, 1, 2, 3, 4, 5 }, 0, 5); Span value = new Span(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 span = new Span(new string[] { "0", "1", "77", "2", "3", "77", "77", "4", "5", "77", "77", "77", "88", "6", "6", "77", "77", "88", "9" }); Span value = new Span(Array.Empty()); 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 span = new Span(new string[] { "0", "1", "2", "3", "4", "5" }); Span value = new Span(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 span = new Span(new string[] { "0", "1", "2", "3", "4", "5" }); Span value = new Span(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 span = new Span(new string[] { "0", "1", "2", "3", "4", "5" }, 0, 5); Span value = new Span(new string[] { "5" }); int index = span.LastIndexOf(value); diff --git a/src/libraries/System.Memory/tests/Span/LastIndexOfSequence.byte.cs b/src/libraries/System.Memory/tests/Span/LastIndexOfSequence.byte.cs index 96b2297..8c8a02d 100644 --- a/src/libraries/System.Memory/tests/Span/LastIndexOfSequence.byte.cs +++ b/src/libraries/System.Memory/tests/Span/LastIndexOfSequence.byte.cs @@ -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 span = new Span(new byte[] { 0, 1, 77, 2, 3, 77, 77, 4, 5, 77, 77, 77, 88, 6, 6, 77, 77, 88, 9 }); Span value = new Span(Array.Empty()); 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 span = new Span(new byte[] { 0, 1, 2, 3, 4, 5 }); Span value = new Span(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 span = new Span(new byte[] { 0, 1, 2, 3, 4, 5 }); Span value = new Span(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 span = new Span(new byte[] { 0, 1, 5, 3, 4, 5 }); Span value = new Span(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 span = new Span(new byte[] { 0, 1, 2, 3, 4, 5 }, 0, 5); Span value = new Span(new byte[] { 5 }); int index = span.LastIndexOf(value); diff --git a/src/libraries/System.Memory/tests/Span/LastIndexOfSequence.char.cs b/src/libraries/System.Memory/tests/Span/LastIndexOfSequence.char.cs index af027cb..362257f 100644 --- a/src/libraries/System.Memory/tests/Span/LastIndexOfSequence.char.cs +++ b/src/libraries/System.Memory/tests/Span/LastIndexOfSequence.char.cs @@ -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 span = new Span(new char[] { '0', '1', '7', '2', '3', '7', '7', '4', '5', '7', '7', '7', '8', '6', '6', '7', '7', '8', '9' }); Span value = new Span(Array.Empty()); 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 span = new Span(new char[] { '0', '1', '2', '3', '4', '5' }); Span value = new Span(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 span = new Span(new char[] { '0', '1', '2', '3', '4', '5' }); Span value = new Span(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 span = new Span(new char[] { '0', '1', '5', '3', '4', '5' }); Span value = new Span(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 span = new Span(new char[] { '0', '1', '2', '3', '4', '5' }, 0, 5); Span value = new Span(new char[] { '5' }); int index = span.LastIndexOf(value); diff --git a/src/libraries/System.Memory/tests/TestHelpers.cs b/src/libraries/System.Memory/tests/TestHelpers.cs index 16b596e..f468c54 100644 --- a/src/libraries/System.Memory/tests/TestHelpers.cs +++ b/src/libraries/System.Memory/tests/TestHelpers.cs @@ -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}, diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Invariant.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Invariant.cs index 663530b..922a471 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Invariant.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Invariant.cs @@ -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) diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Unix.cs index 4158890..d0db147 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Unix.cs @@ -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) diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Windows.cs index 897cb5b..cbc7ac0 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Windows.cs @@ -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) { diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.cs index e0b4da0..57c6f81 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.cs @@ -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) diff --git a/src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.Globalization.cs b/src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.Globalization.cs index 604035e..fb339f7 100644 --- a/src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.Globalization.cs +++ b/src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.Globalization.cs @@ -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) diff --git a/src/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs b/src/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs index 4d84fd3..025d22e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs +++ b/src/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs @@ -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); diff --git a/src/libraries/System.Private.CoreLib/src/System/SpanHelpers.T.cs b/src/libraries/System.Private.CoreLib/src/System/SpanHelpers.T.cs index 0b354c3..4177c45 100644 --- a/src/libraries/System.Private.CoreLib/src/System/SpanHelpers.T.cs +++ b/src/libraries/System.Private.CoreLib/src/System/SpanHelpers.T.cs @@ -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); diff --git a/src/libraries/System.Private.CoreLib/src/System/String.Comparison.cs b/src/libraries/System.Private.CoreLib/src/System/String.Comparison.cs index b664044..5ceed67 100644 --- a/src/libraries/System.Private.CoreLib/src/System/String.Comparison.cs +++ b/src/libraries/System.Private.CoreLib/src/System/String.Comparison.cs @@ -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 + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/String.Searching.cs b/src/libraries/System.Private.CoreLib/src/System/String.Searching.cs index e80cd43..93de7fa 100644 --- a/src/libraries/System.Private.CoreLib/src/System/String.Searching.cs +++ b/src/libraries/System.Private.CoreLib/src/System/String.Searching.cs @@ -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 = "hihi" (where = 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 "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 = ), 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 = "hihi" (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 "hihi".LastIndexOf("hi", comparer) can evaluate to any of { 2, 3, 4 }. + * + * If there exists in set S any entry with the tuple values (startPos = , 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)); } }