// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; using System.Buffers.Binary; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; using System.Text.Unicode; namespace System { public partial class String { // // Search/Query methods // [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool EqualsHelper(string strA, string strB) { Debug.Assert(strA != null); Debug.Assert(strB != null); Debug.Assert(strA.Length == strB.Length); return SpanHelpers.SequenceEqual( ref strA.GetRawStringDataAsUInt8(), ref strB.GetRawStringDataAsUInt8(), ((uint)strA.Length) * sizeof(char)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int CompareOrdinalHelper(string strA, int indexA, int countA, string strB, int indexB, int countB) { Debug.Assert(strA != null); Debug.Assert(strB != null); Debug.Assert(indexA >= 0 && indexB >= 0); Debug.Assert(countA >= 0 && countB >= 0); Debug.Assert(indexA + countA <= strA.Length && indexB + countB <= strB.Length); return SpanHelpers.SequenceCompareTo( ref Unsafe.Add(ref strA.GetRawStringData(), (nint)(uint)indexA /* force zero-extension */), countA, ref Unsafe.Add(ref strB.GetRawStringData(), (nint)(uint)indexB /* force zero-extension */), countB); } private static bool EqualsOrdinalIgnoreCaseNoLengthCheck(string strA, string strB) { Debug.Assert(strA.Length == strB.Length); return Ordinal.EqualsIgnoreCase(ref strA.GetRawStringData(), ref strB.GetRawStringData(), strB.Length); } private static int CompareOrdinalHelper(string strA, string strB) { Debug.Assert(strA != null); Debug.Assert(strB != null); if (strA._firstChar != strB._firstChar) goto DiffOffset0; // The reason we check if the second char is different is because // if the first two chars the same we can increment by 4 bytes, // leaving us word-aligned on both 32-bit (12 bytes into the string) // and 64-bit (16 bytes) platforms. // For empty strings, the second char will be null due to padding. // The start of the string is the type pointer + string length, which // takes up 8 bytes on 32-bit, 12 on x64. For empty strings the null // terminator immediately follows, leaving us with an object // 10/14 bytes in size. Since everything needs to be a multiple // of 4/8, this will get padded and zeroed out. // For one-char strings the second char will be the null terminator. if (Unsafe.Add(ref strA._firstChar, 1) != Unsafe.Add(ref strB._firstChar, 1)) goto DiffOffset1; if (Math.Min(strA.Length, strB.Length) <= 2) goto NotLongerThan2; // Since we know that the first two chars are the same, // we can increment by 2 here and skip 4 bytes. // This leaves us 8-byte aligned, which results // on better perf for 64-bit platforms and SIMD. return SpanHelpers.SequenceCompareTo( ref Unsafe.Add(ref strA._firstChar, 2), strA.Length - 2, ref Unsafe.Add(ref strB._firstChar, 2), strB.Length - 2); NotLongerThan2: // The first two chars are the same, and the shorter string is not longer // than two chars, then the two strings can only differ by length. return strA.Length - strB.Length; DiffOffset0: return strA._firstChar - strB._firstChar; DiffOffset1: return Unsafe.Add(ref strA._firstChar, 1) - Unsafe.Add(ref strB._firstChar, 1); } // Provides a culture-correct string comparison. StrA is compared to StrB // to determine whether it is lexicographically less, equal, or greater, and then returns // either a negative integer, 0, or a positive integer; respectively. // public static int Compare(string? strA, string? strB) { return Compare(strA, strB, StringComparison.CurrentCulture); } // Provides a culture-correct string comparison. strA is compared to strB // to determine whether it is lexicographically less, equal, or greater, and then a // negative integer, 0, or a positive integer is returned; respectively. // The case-sensitive option is set by ignoreCase // public static int Compare(string? strA, string? strB, bool ignoreCase) { StringComparison comparisonType = ignoreCase ? StringComparison.CurrentCultureIgnoreCase : StringComparison.CurrentCulture; return Compare(strA, strB, comparisonType); } // Provides a more flexible function for string comparison. See StringComparison // for meaning of different comparisonType. public static int Compare(string? strA, string? strB, StringComparison comparisonType) { if (ReferenceEquals(strA, strB)) { CheckStringComparison(comparisonType); return 0; } // They can't both be null at this point. if (strA == null) { CheckStringComparison(comparisonType); return -1; } if (strB == null) { CheckStringComparison(comparisonType); return 1; } switch (comparisonType) { case StringComparison.CurrentCulture: case StringComparison.CurrentCultureIgnoreCase: return CultureInfo.CurrentCulture.CompareInfo.Compare(strA, strB, GetCaseCompareOfComparisonCulture(comparisonType)); case StringComparison.InvariantCulture: case StringComparison.InvariantCultureIgnoreCase: return CompareInfo.Invariant.Compare(strA, strB, GetCaseCompareOfComparisonCulture(comparisonType)); case StringComparison.Ordinal: return CompareOrdinalHelper(strA, strB); case StringComparison.OrdinalIgnoreCase: return Ordinal.CompareStringIgnoreCase(ref strA.GetRawStringData(), strA.Length, ref strB.GetRawStringData(), strB.Length); default: throw new ArgumentException(SR.NotSupported_StringComparison, nameof(comparisonType)); } } // Provides a culture-correct string comparison. strA is compared to strB // to determine whether it is lexicographically less, equal, or greater, and then a // negative integer, 0, or a positive integer is returned; respectively. // public static int Compare(string? strA, string? strB, CultureInfo? culture, CompareOptions options) { CultureInfo compareCulture = culture ?? CultureInfo.CurrentCulture; return compareCulture.CompareInfo.Compare(strA, strB, options); } // Provides a culture-correct string comparison. strA is compared to strB // to determine whether it is lexicographically less, equal, or greater, and then a // negative integer, 0, or a positive integer is returned; respectively. // The case-sensitive option is set by ignoreCase, and the culture is set // by culture // public static int Compare(string? strA, string? strB, bool ignoreCase, CultureInfo? culture) { CompareOptions options = ignoreCase ? CompareOptions.IgnoreCase : CompareOptions.None; return Compare(strA, strB, culture, options); } // Determines whether two string regions match. The substring of strA beginning // at indexA of given length is compared with the substring of strB // beginning at indexB of the same length. // public static int Compare(string? strA, int indexA, string? strB, int indexB, int length) { // NOTE: It's important we call the boolean overload, and not the StringComparison // one. The two have some subtly different behavior (see notes in the former). return Compare(strA, indexA, strB, indexB, length, ignoreCase: false); } // Determines whether two string regions match. The substring of strA beginning // at indexA of given length is compared with the substring of strB // beginning at indexB of the same length. Case sensitivity is determined by the ignoreCase boolean. // public static int Compare(string? strA, int indexA, string? strB, int indexB, int length, bool ignoreCase) { // Ideally we would just forward to the string.Compare overload that takes // a StringComparison parameter, and just pass in CurrentCulture/CurrentCultureIgnoreCase. // That function will return early if an optimization can be applied, e.g. if // (object)strA == strB && indexA == indexB then it will return 0 straightaway. // There are a couple of subtle behavior differences that prevent us from doing so // however: // - string.Compare(null, -1, null, -1, -1, StringComparison.CurrentCulture) works // since that method also returns early for nulls before validation. It shouldn't // for this overload. // - Since we originally forwarded to CompareInfo.Compare for all of the argument // validation logic, the ArgumentOutOfRangeExceptions thrown will contain different // parameter names. // Therefore, we have to duplicate some of the logic here. int lengthA = length; int lengthB = length; if (strA != null) { lengthA = Math.Min(lengthA, strA.Length - indexA); } if (strB != null) { lengthB = Math.Min(lengthB, strB.Length - indexB); } CompareOptions options = ignoreCase ? CompareOptions.IgnoreCase : CompareOptions.None; return CultureInfo.CurrentCulture.CompareInfo.Compare(strA, indexA, lengthA, strB, indexB, lengthB, options); } // Determines whether two string regions match. The substring of strA beginning // at indexA of length length is compared with the substring of strB // beginning at indexB of the same length. Case sensitivity is determined by the ignoreCase boolean, // and the culture is set by culture. // public static int Compare(string? strA, int indexA, string? strB, int indexB, int length, bool ignoreCase, CultureInfo? culture) { CompareOptions options = ignoreCase ? CompareOptions.IgnoreCase : CompareOptions.None; return Compare(strA, indexA, strB, indexB, length, culture, options); } // Determines whether two string regions match. The substring of strA beginning // at indexA of length length is compared with the substring of strB // beginning at indexB of the same length. // public static int Compare(string? strA, int indexA, string? strB, int indexB, int length, CultureInfo? culture, CompareOptions options) { CultureInfo compareCulture = culture ?? CultureInfo.CurrentCulture; int lengthA = length; int lengthB = length; if (strA != null) { lengthA = Math.Min(lengthA, strA.Length - indexA); } if (strB != null) { lengthB = Math.Min(lengthB, strB.Length - indexB); } return compareCulture.CompareInfo.Compare(strA, indexA, lengthA, strB, indexB, lengthB, options); } public static int Compare(string? strA, int indexA, string? strB, int indexB, int length, StringComparison comparisonType) { CheckStringComparison(comparisonType); if (strA == null || strB == null) { if (ReferenceEquals(strA, strB)) { // They're both null return 0; } return strA == null ? -1 : 1; } ArgumentOutOfRangeException.ThrowIfNegative(length); if (indexA < 0 || indexB < 0) { string paramName = indexA < 0 ? nameof(indexA) : nameof(indexB); throw new ArgumentOutOfRangeException(paramName, SR.ArgumentOutOfRange_IndexMustBeLessOrEqual); } if (strA.Length - indexA < 0 || strB.Length - indexB < 0) { string paramName = strA.Length - indexA < 0 ? nameof(indexA) : nameof(indexB); throw new ArgumentOutOfRangeException(paramName, SR.ArgumentOutOfRange_IndexMustBeLessOrEqual); } if (length == 0 || (ReferenceEquals(strA, strB) && indexA == indexB)) { return 0; } int lengthA = Math.Min(length, strA.Length - indexA); int lengthB = Math.Min(length, strB.Length - indexB); switch (comparisonType) { case StringComparison.CurrentCulture: case StringComparison.CurrentCultureIgnoreCase: return CultureInfo.CurrentCulture.CompareInfo.Compare(strA, indexA, lengthA, strB, indexB, lengthB, GetCaseCompareOfComparisonCulture(comparisonType)); case StringComparison.InvariantCulture: case StringComparison.InvariantCultureIgnoreCase: return CompareInfo.Invariant.Compare(strA, indexA, lengthA, strB, indexB, lengthB, GetCaseCompareOfComparisonCulture(comparisonType)); case StringComparison.Ordinal: return CompareOrdinalHelper(strA, indexA, lengthA, strB, indexB, lengthB); default: Debug.Assert(comparisonType == StringComparison.OrdinalIgnoreCase); // CheckStringComparison validated these earlier return Ordinal.CompareStringIgnoreCase(ref Unsafe.Add(ref strA.GetRawStringData(), indexA), lengthA, ref Unsafe.Add(ref strB.GetRawStringData(), indexB), lengthB); } } // Compares strA and strB using an ordinal (code-point) comparison. // public static int CompareOrdinal(string? strA, string? strB) { if (ReferenceEquals(strA, strB)) { return 0; } // They can't both be null at this point. if (strA == null) { return -1; } if (strB == null) { return 1; } return CompareOrdinalHelper(strA, strB); } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static int CompareOrdinal(ReadOnlySpan<char> strA, ReadOnlySpan<char> strB) => SpanHelpers.SequenceCompareTo(ref MemoryMarshal.GetReference(strA), strA.Length, ref MemoryMarshal.GetReference(strB), strB.Length); // Compares strA and strB using an ordinal (code-point) comparison. // public static int CompareOrdinal(string? strA, int indexA, string? strB, int indexB, int length) { if (strA == null || strB == null) { if (ReferenceEquals(strA, strB)) { // They're both null return 0; } return strA == null ? -1 : 1; } // COMPAT: Checking for nulls should become before the arguments are validated, // but other optimizations which allow us to return early should come after. ArgumentOutOfRangeException.ThrowIfNegative(length); if (indexA < 0 || indexB < 0) { string paramName = indexA < 0 ? nameof(indexA) : nameof(indexB); throw new ArgumentOutOfRangeException(paramName, SR.ArgumentOutOfRange_IndexMustBeLessOrEqual); } int lengthA = Math.Min(length, strA.Length - indexA); int lengthB = Math.Min(length, strB.Length - indexB); if (lengthA < 0 || lengthB < 0) { string paramName = lengthA < 0 ? nameof(indexA) : nameof(indexB); throw new ArgumentOutOfRangeException(paramName, SR.ArgumentOutOfRange_IndexMustBeLessOrEqual); } if (length == 0 || (ReferenceEquals(strA, strB) && indexA == indexB)) { return 0; } return CompareOrdinalHelper(strA, indexA, lengthA, strB, indexB, lengthB); } // Compares this String to another String (cast as object), returning an integer that // indicates the relationship. This method returns a value less than 0 if this is less than value, 0 // if this is equal to value, or a value greater than 0 if this is greater than value. // public int CompareTo(object? value) { if (value == null) { return 1; } if (value is not string other) { throw new ArgumentException(SR.Arg_MustBeString); } return CompareTo(other); // will call the string-based overload } // Determines the sorting relation of StrB to the current instance. // public int CompareTo(string? strB) { return Compare(this, strB, StringComparison.CurrentCulture); } // Determines whether a specified string is a suffix of the current instance. // // The case-sensitive and culture-sensitive option is set by options, // and the default culture is used. // public bool EndsWith(string value) { return EndsWith(value, StringComparison.CurrentCulture); } [Intrinsic] // Unrolled and vectorized for half-constant input (Ordinal) public bool EndsWith(string value, StringComparison comparisonType) { ArgumentNullException.ThrowIfNull(value); if (ReferenceEquals(this, value)) { CheckStringComparison(comparisonType); return true; } if (value.Length == 0) { CheckStringComparison(comparisonType); return true; } switch (comparisonType) { case StringComparison.CurrentCulture: case StringComparison.CurrentCultureIgnoreCase: return CultureInfo.CurrentCulture.CompareInfo.IsSuffix(this, value, GetCaseCompareOfComparisonCulture(comparisonType)); case StringComparison.InvariantCulture: case StringComparison.InvariantCultureIgnoreCase: return CompareInfo.Invariant.IsSuffix(this, value, GetCaseCompareOfComparisonCulture(comparisonType)); case StringComparison.Ordinal: int offset = this.Length - value.Length; return (uint)offset <= (uint)this.Length && this.AsSpan(offset).SequenceEqual(value); case StringComparison.OrdinalIgnoreCase: return Length >= value.Length && Ordinal.EqualsIgnoreCase(ref Unsafe.Add(ref GetRawStringData(), Length - value.Length), ref value.GetRawStringData(), value.Length); default: throw new ArgumentException(SR.NotSupported_StringComparison, nameof(comparisonType)); } } public bool EndsWith(string value, bool ignoreCase, CultureInfo? culture) { ArgumentNullException.ThrowIfNull(value); if (ReferenceEquals(this, value)) { return true; } CultureInfo referenceCulture = culture ?? CultureInfo.CurrentCulture; return referenceCulture.CompareInfo.IsSuffix(this, value, ignoreCase ? CompareOptions.IgnoreCase : CompareOptions.None); } public bool EndsWith(char value) { // If the string is empty, *(&_firstChar + length - 1) will deref within // the _stringLength field, which will be all-zero. We must forbid '\0' // from going down the optimized code path because otherwise empty strings // would appear to end with '\0', which is incorrect. // n.b. This optimization relies on the layout of string and is not valid // for other data types like char[] or Span<char>. if (RuntimeHelpers.IsKnownConstant(value) && value != '\0') { // deref Length now to front-load the null check; also take this time to zero-extend // n.b. (localLength - 1) could be negative! nuint localLength = (uint)Length; return Unsafe.Add(ref _firstChar, (nint)localLength - 1) == value; } int lastPos = Length - 1; return ((uint)lastPos < (uint)Length) && this[lastPos] == value; } /// <summary> /// Determines whether the end of this string instance matches the specified character. /// </summary> /// <param name="value">The character to compare to the character at the end of this instance.</param> /// <param name="comparisonType">One of the enumeration values that specifies the rules to use in the comparison.</param> /// <returns><see langword="true"/> if <paramref name="value"/> matches the end of this instance; otherwise, <see langword="false"/>.</returns> public bool EndsWith(char value, StringComparison comparisonType) { // Convert value to span ReadOnlySpan<char> valueChars = [value]; return this.EndsWith(valueChars, comparisonType); } /// <summary> /// Determines whether the end of this string instance matches the specified rune using an ordinal comparison. /// </summary> /// <param name="value">The character to compare to the character at the end of this instance.</param> /// <returns><see langword="true"/> if <paramref name="value"/> matches the end of this instance; otherwise, <see langword="false"/>.</returns> public bool EndsWith(Rune value) { if (value.IsBmp) { return EndsWith((char)value.Value); } UnicodeUtility.GetUtf16SurrogatesFromSupplementaryPlaneScalar((uint)value.Value, out char highSurrogate, out char lowSurrogate); return Length > 1 && this[^2] == highSurrogate && this[^1] == lowSurrogate; } /// <summary> /// Determines whether the end of this string instance matches the specified rune when compared using the specified comparison option. /// </summary> /// <param name="value">The character to compare to the character at the end of this instance.</param> /// <param name="comparisonType">One of the enumeration values that specifies the rules to use in the comparison.</param> /// <returns><see langword="true"/> if <paramref name="value"/> matches the end of this instance; otherwise, <see langword="false"/>.</returns> public unsafe bool EndsWith(Rune value, StringComparison comparisonType) { // Convert value to span ReadOnlySpan<char> valueChars = value.AsSpan(stackalloc char[Rune.MaxUtf16CharsPerRune]); return this.EndsWith(valueChars, comparisonType); } // Determines whether two strings match. public override bool Equals([NotNullWhen(true)] object? obj) { if (ReferenceEquals(this, obj)) return true; if (obj is not string str) return false; if (this.Length != str.Length) return false; return EqualsHelper(this, str); } // Determines whether two strings match. [Intrinsic] // Unrolled and vectorized for half-constant input public bool Equals([NotNullWhen(true)] string? value) { if (ReferenceEquals(this, value)) return true; // NOTE: No need to worry about casting to object here. // If either side of an == comparison between strings // is null, Roslyn generates a simple ceq instruction // instead of calling string.op_Equality. if (value == null) return false; if (this.Length != value.Length) return false; return EqualsHelper(this, value); } [Intrinsic] // Unrolled and vectorized for half-constant input (Ordinal) public bool Equals([NotNullWhen(true)] string? value, StringComparison comparisonType) { if (ReferenceEquals(this, value)) { CheckStringComparison(comparisonType); return true; } if (value is null) { CheckStringComparison(comparisonType); return false; } switch (comparisonType) { case StringComparison.CurrentCulture: case StringComparison.CurrentCultureIgnoreCase: return CultureInfo.CurrentCulture.CompareInfo.Compare(this, value, GetCaseCompareOfComparisonCulture(comparisonType)) == 0; case StringComparison.InvariantCulture: case StringComparison.InvariantCultureIgnoreCase: return CompareInfo.Invariant.Compare(this, value, GetCaseCompareOfComparisonCulture(comparisonType)) == 0; case StringComparison.Ordinal: if (this.Length != value.Length) return false; return EqualsHelper(this, value); case StringComparison.OrdinalIgnoreCase: if (this.Length != value.Length) return false; return EqualsOrdinalIgnoreCaseNoLengthCheck(this, value); default: throw new ArgumentException(SR.NotSupported_StringComparison, nameof(comparisonType)); } } // Determines whether two Strings match. [Intrinsic] // Unrolled and vectorized for half-constant input public static bool Equals(string? a, string? b) { if (ReferenceEquals(a, b)) { return true; } if (a is null || b is null || a.Length != b.Length) { return false; } return EqualsHelper(a, b); } [Intrinsic] // Unrolled and vectorized for half-constant input (Ordinal) public static bool Equals(string? a, string? b, StringComparison comparisonType) { if (ReferenceEquals(a, b)) { CheckStringComparison(comparisonType); return true; } if (a is null || b is null) { CheckStringComparison(comparisonType); return false; } switch (comparisonType) { case StringComparison.CurrentCulture: case StringComparison.CurrentCultureIgnoreCase: return CultureInfo.CurrentCulture.CompareInfo.Compare(a, b, GetCaseCompareOfComparisonCulture(comparisonType)) == 0; case StringComparison.InvariantCulture: case StringComparison.InvariantCultureIgnoreCase: return CompareInfo.Invariant.Compare(a, b, GetCaseCompareOfComparisonCulture(comparisonType)) == 0; case StringComparison.Ordinal: if (a.Length != b.Length) return false; return EqualsHelper(a, b); case StringComparison.OrdinalIgnoreCase: if (a.Length != b.Length) return false; return EqualsOrdinalIgnoreCaseNoLengthCheck(a, b); default: throw new ArgumentException(SR.NotSupported_StringComparison, nameof(comparisonType)); } } public static bool operator ==(string? a, string? b) => Equals(a, b); public static bool operator !=(string? a, string? b) => !Equals(a, b); // Gets a hash code for this string. If strings A and B are such that A.Equals(B), then // they will return the same hash code. [MethodImpl(MethodImplOptions.AggressiveInlining)] public override int GetHashCode() { ulong seed = Marvin.DefaultSeed; // Multiplication below will not overflow since going from positive Int32 to UInt32. return Marvin.ComputeHash32(ref Unsafe.As<char, byte>(ref _firstChar), (uint)_stringLength * 2 /* in bytes, not chars */, (uint)seed, (uint)(seed >> 32)); } // Gets a hash code for this string and this comparison. If strings A and B and comparison C are such // that string.Equals(A, B, C), then they will return the same hash code with this comparison C. public int GetHashCode(StringComparison comparisonType) => StringComparer.FromComparison(comparisonType).GetHashCode(this); [MethodImpl(MethodImplOptions.AggressiveInlining)] internal int GetHashCodeOrdinalIgnoreCase() { ulong seed = Marvin.DefaultSeed; return Marvin.ComputeHash32OrdinalIgnoreCase(ref _firstChar, _stringLength /* in chars, not bytes */, (uint)seed, (uint)(seed >> 32)); } // A span-based equivalent of String.GetHashCode(). Computes an ordinal hash code. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int GetHashCode(ReadOnlySpan<char> value) { ulong seed = Marvin.DefaultSeed; // Multiplication below will not overflow since going from positive Int32 to UInt32. return Marvin.ComputeHash32(ref Unsafe.As<char, byte>(ref MemoryMarshal.GetReference(value)), (uint)value.Length * 2 /* in bytes, not chars */, (uint)seed, (uint)(seed >> 32)); } // A span-based equivalent of String.GetHashCode(StringComparison). Uses the specified comparison type. public static int GetHashCode(ReadOnlySpan<char> value, StringComparison comparisonType) { switch (comparisonType) { case StringComparison.CurrentCulture: case StringComparison.CurrentCultureIgnoreCase: return CultureInfo.CurrentCulture.CompareInfo.GetHashCode(value, GetCaseCompareOfComparisonCulture(comparisonType)); case StringComparison.InvariantCulture: case StringComparison.InvariantCultureIgnoreCase: return CompareInfo.Invariant.GetHashCode(value, GetCaseCompareOfComparisonCulture(comparisonType)); case StringComparison.Ordinal: return GetHashCode(value); case StringComparison.OrdinalIgnoreCase: return GetHashCodeOrdinalIgnoreCase(value); default: ThrowHelper.ThrowArgumentException(ExceptionResource.NotSupported_StringComparison, ExceptionArgument.comparisonType); Debug.Fail("Should not reach this point."); return default; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static int GetHashCodeOrdinalIgnoreCase(ReadOnlySpan<char> value) { ulong seed = Marvin.DefaultSeed; return Marvin.ComputeHash32OrdinalIgnoreCase(ref MemoryMarshal.GetReference(value), value.Length /* in chars, not bytes */, (uint)seed, (uint)(seed >> 32)); } // Important GetNonRandomizedHashCode{OrdinalIgnoreCase} notes: // // Use if and only if 'Denial of Service' attacks are not a concern (i.e. never used for free-form user input), // or are otherwise mitigated. // // The string-based implementation relies on System.String being null terminated. All reads are performed // two characters at a time, so for odd-length strings, the final read will include the null terminator. // This implementation must not be used as-is with spans, or otherwise arbitrary char refs/pointers, as // they're not guaranteed to be null-terminated. // // For spans, we must produce the exact same value as is used for strings: consumers like Dictionary<> // rely on str.GetNonRandomizedHashCode() == GetNonRandomizedHashCode(str.AsSpan()). As such, we must // restructure the comparison so that for odd-length spans, we simulate the null terminator and include // it in the hash computation exactly as does str.GetNonRandomizedHashCode(). internal unsafe int GetNonRandomizedHashCode() { fixed (char* src = &_firstChar) { Debug.Assert(src[Length] == '\0', "src[Length] == '\\0'"); Debug.Assert(((int)src) % 4 == 0, "Managed string should start at 4 bytes boundary"); uint hash1 = (5381 << 16) + 5381; uint hash2 = hash1; uint* ptr = (uint*)src; int length = Length; while (length > 2) { length -= 4; hash1 = (BitOperations.RotateLeft(hash1, 5) + hash1) ^ ptr[0]; hash2 = (BitOperations.RotateLeft(hash2, 5) + hash2) ^ ptr[1]; ptr += 2; } if (length > 0) { hash2 = (BitOperations.RotateLeft(hash2, 5) + hash2) ^ ptr[0]; } return (int)(hash1 + (hash2 * 1566083941)); } } internal static unsafe int GetNonRandomizedHashCode(ReadOnlySpan<char> span) { uint hash1 = (5381 << 16) + 5381; uint hash2 = hash1; int length = span.Length; fixed (char* src = &MemoryMarshal.GetReference(span)) { uint* ptr = (uint*)src; LengthSwitch: switch (length) { default: do { length -= 4; hash1 = BitOperations.RotateLeft(hash1, 5) + hash1 ^ Unsafe.ReadUnaligned<uint>(ptr); hash2 = BitOperations.RotateLeft(hash2, 5) + hash2 ^ Unsafe.ReadUnaligned<uint>(ptr + 1); ptr += 2; } while (length >= 4); goto LengthSwitch; case 3: hash1 = BitOperations.RotateLeft(hash1, 5) + hash1 ^ Unsafe.ReadUnaligned<uint>(ptr); uint p1 = *(char*)(ptr + 1); if (!BitConverter.IsLittleEndian) { p1 <<= 16; } hash2 = BitOperations.RotateLeft(hash2, 5) + hash2 ^ p1; break; case 2: hash2 = BitOperations.RotateLeft(hash2, 5) + hash2 ^ Unsafe.ReadUnaligned<uint>(ptr); break; case 1: uint p0 = *(char*)ptr; if (!BitConverter.IsLittleEndian) { p0 <<= 16; } hash2 = BitOperations.RotateLeft(hash2, 5) + hash2 ^ p0; break; case 0: break; } } return (int)(hash1 + (hash2 * 1_566_083_941)); } // We "normalize to lowercase" every char by ORing with 0x0020. This casts // a very wide net because it will change, e.g., '^' to '~'. But that should // be ok because we expect this to be very rare in practice. These are valid // for both for big-endian and for little-endian. private const uint NormalizeToLowercase = 0x0020_0020u; internal unsafe int GetNonRandomizedHashCodeOrdinalIgnoreCase() { uint hash1 = (5381 << 16) + 5381; uint hash2 = hash1; int length = Length; fixed (char* src = &_firstChar) { Debug.Assert(src[Length] == '\0', "src[this.Length] == '\\0'"); Debug.Assert(((int) src) % 4 == 0, "Managed string should start at 4 bytes boundary"); uint* ptr = (uint*) src; while (length > 2) { uint p0 = ptr[0]; uint p1 = ptr[1]; if (!Utf16Utility.AllCharsInUInt32AreAscii(p0 | p1)) { goto NotAscii; } length -= 4; hash1 = (BitOperations.RotateLeft(hash1, 5) + hash1) ^ (p0 | NormalizeToLowercase); hash2 = (BitOperations.RotateLeft(hash2, 5) + hash2) ^ (p1 | NormalizeToLowercase); ptr += 2; } if (length > 0) { uint p0 = ptr[0]; if (!Utf16Utility.AllCharsInUInt32AreAscii(p0)) { goto NotAscii; } hash2 = (BitOperations.RotateLeft(hash2, 5) + hash2) ^ (p0 | NormalizeToLowercase); } } return (int)(hash1 + (hash2 * 1566083941)); NotAscii: return GetNonRandomizedHashCodeOrdinalIgnoreCaseSlow(hash1, hash2, this.AsSpan(Length - length)); } internal static unsafe int GetNonRandomizedHashCodeOrdinalIgnoreCase(ReadOnlySpan<char> span) { uint hash1 = (5381 << 16) + 5381; uint hash2 = hash1; uint p0, p1; int length = span.Length; fixed (char* src = &MemoryMarshal.GetReference(span)) { uint* ptr = (uint*)src; LengthSwitch: switch (length) { default: do { p0 = Unsafe.ReadUnaligned<uint>(ptr); p1 = Unsafe.ReadUnaligned<uint>(ptr + 1); if (!Utf16Utility.AllCharsInUInt32AreAscii(p0 | p1)) { goto NotAscii; } length -= 4; hash1 = (BitOperations.RotateLeft(hash1, 5) + hash1) ^ (p0 | NormalizeToLowercase); hash2 = (BitOperations.RotateLeft(hash2, 5) + hash2) ^ (p1 | NormalizeToLowercase); ptr += 2; } while (length >= 4); goto LengthSwitch; case 3: p0 = Unsafe.ReadUnaligned<uint>(ptr); p1 = *(char*)(ptr + 1); if (!BitConverter.IsLittleEndian) { p1 <<= 16; } if (!Utf16Utility.AllCharsInUInt32AreAscii(p0 | p1)) { goto NotAscii; } hash1 = (BitOperations.RotateLeft(hash1, 5) + hash1) ^ (p0 | NormalizeToLowercase); hash2 = (BitOperations.RotateLeft(hash2, 5) + hash2) ^ (p1 | NormalizeToLowercase); break; case 2: p0 = Unsafe.ReadUnaligned<uint>(ptr); if (!Utf16Utility.AllCharsInUInt32AreAscii(p0)) { goto NotAscii; } hash2 = (BitOperations.RotateLeft(hash2, 5) + hash2) ^ (p0 | NormalizeToLowercase); break; case 1: p0 = *(char*)ptr; if (!BitConverter.IsLittleEndian) { p0 <<= 16; } if (p0 > 0x7f) { goto NotAscii; } hash2 = (BitOperations.RotateLeft(hash2, 5) + hash2) ^ (p0 | NormalizeToLowercase); break; case 0: break; } } return (int)(hash1 + (hash2 * 1566083941)); NotAscii: return GetNonRandomizedHashCodeOrdinalIgnoreCaseSlow(hash1, hash2, span.Slice(span.Length - length)); } private static unsafe int GetNonRandomizedHashCodeOrdinalIgnoreCaseSlow(uint hash1, uint hash2, ReadOnlySpan<char> str) { int length = str.Length; // We allocate one char more than the length to accommodate a null terminator. // That lets the reading always be performed two characters at a time, as odd-length // inputs will have a final terminator to backstop the last read. char[]? borrowedArr = null; Span<char> scratch = (uint)length < 256 ? stackalloc char[256] : (borrowedArr = ArrayPool<char>.Shared.Rent(length + 1)); int charsWritten = Ordinal.ToUpperOrdinal(str, scratch); Debug.Assert(charsWritten == length); scratch[length] = '\0'; // Duplicate the main loop, can be removed once JIT gets "Loop Unswitching" optimization fixed (char* src = scratch) { uint* ptr = (uint*)src; while (length > 2) { length -= 4; hash1 = (BitOperations.RotateLeft(hash1, 5) + hash1) ^ (ptr[0] | NormalizeToLowercase); hash2 = (BitOperations.RotateLeft(hash2, 5) + hash2) ^ (ptr[1] | NormalizeToLowercase); ptr += 2; } if (length > 0) { hash2 = (BitOperations.RotateLeft(hash2, 5) + hash2) ^ (ptr[0] | NormalizeToLowercase); } } if (borrowedArr != null) { ArrayPool<char>.Shared.Return(borrowedArr); } return (int)(hash1 + (hash2 * 1566083941)); } // Determines whether a specified string is a prefix of the current instance // public bool StartsWith(string value) { ArgumentNullException.ThrowIfNull(value); return StartsWith(value, StringComparison.CurrentCulture); } [Intrinsic] // Unrolled and vectorized for half-constant input (Ordinal) public bool StartsWith(string value, StringComparison comparisonType) { ArgumentNullException.ThrowIfNull(value); if (ReferenceEquals(this, value)) { CheckStringComparison(comparisonType); return true; } if (value.Length == 0) { CheckStringComparison(comparisonType); return true; } switch (comparisonType) { case StringComparison.CurrentCulture: case StringComparison.CurrentCultureIgnoreCase: return CultureInfo.CurrentCulture.CompareInfo.IsPrefix(this, value, GetCaseCompareOfComparisonCulture(comparisonType)); case StringComparison.InvariantCulture: case StringComparison.InvariantCultureIgnoreCase: return CompareInfo.Invariant.IsPrefix(this, value, GetCaseCompareOfComparisonCulture(comparisonType)); case StringComparison.Ordinal: if (this.Length < value.Length || _firstChar != value._firstChar) { return false; } return (value.Length == 1) ? true : // First char is the same and thats all there is to compare SpanHelpers.SequenceEqual( ref this.GetRawStringDataAsUInt8(), ref value.GetRawStringDataAsUInt8(), ((nuint)value.Length) * 2); case StringComparison.OrdinalIgnoreCase: if (this.Length < value.Length) { return false; } return Ordinal.EqualsIgnoreCase(ref this.GetRawStringData(), ref value.GetRawStringData(), value.Length); default: throw new ArgumentException(SR.NotSupported_StringComparison, nameof(comparisonType)); } } public bool StartsWith(string value, bool ignoreCase, CultureInfo? culture) { ArgumentNullException.ThrowIfNull(value); if (ReferenceEquals(this, value)) { return true; } CultureInfo referenceCulture = culture ?? CultureInfo.CurrentCulture; return referenceCulture.CompareInfo.IsPrefix(this, value, ignoreCase ? CompareOptions.IgnoreCase : CompareOptions.None); } public bool StartsWith(char value) { // If the string is empty, _firstChar will contain the null terminator. // We forbid '\0' from going down the optimized code path because otherwise // empty strings would appear to begin with '\0', which is incorrect. // n.b. This optimization relies on the layout of string and is not valid // for other data types like char[] or Span<char>. if (RuntimeHelpers.IsKnownConstant(value) && value != '\0') { return _firstChar == value; } return Length != 0 && _firstChar == value; } /// <summary> /// Determines whether the beginning of this string instance matches the specified character when compared using the specified comparison option. /// </summary> /// <param name="value">The character to compare.</param> /// <param name="comparisonType">One of the enumeration values that determines how this string and <paramref name="value"/> are compared.</param> /// <returns><see langword="true"/> if value matches the beginning of this string; otherwise, <see langword="false"/>.</returns> public bool StartsWith(char value, StringComparison comparisonType) { // Convert value to span ReadOnlySpan<char> valueChars = [value]; return this.StartsWith(valueChars, comparisonType); } /// <summary> /// Determines whether the beginning of this string instance matches the specified rune using an ordinal comparison. /// </summary> /// <param name="value">The rune to compare.</param> /// <returns><see langword="true"/> if value matches the beginning of this string; otherwise, <see langword="false"/>.</returns> public bool StartsWith(Rune value) { if (value.IsBmp) { return StartsWith((char)value.Value); } UnicodeUtility.GetUtf16SurrogatesFromSupplementaryPlaneScalar((uint)value.Value, out char highSurrogate, out char lowSurrogate); return Length > 1 && _firstChar == highSurrogate && this[1] == lowSurrogate; } /// <summary> /// Determines whether the beginning of this string instance matches the specified rune when compared using the specified comparison option. /// </summary> /// <param name="value">The rune to compare.</param> /// <param name="comparisonType">One of the enumeration values that determines how this string and <paramref name="value"/> are compared.</param> /// <returns><see langword="true"/> if value matches the beginning of this string; otherwise, <see langword="false"/>.</returns> public unsafe bool StartsWith(Rune value, StringComparison comparisonType) { // Convert value to span ReadOnlySpan<char> valueChars = value.AsSpan(stackalloc char[Rune.MaxUtf16CharsPerRune]); return this.StartsWith(valueChars, comparisonType); } internal static void CheckStringComparison(StringComparison comparisonType) { // Single comparison to check if comparisonType is within [CurrentCulture .. OrdinalIgnoreCase] if ((uint)comparisonType > (uint)StringComparison.OrdinalIgnoreCase) { ThrowHelper.ThrowArgumentException(ExceptionResource.NotSupported_StringComparison, ExceptionArgument.comparisonType); } } internal static CompareOptions GetCaseCompareOfComparisonCulture(StringComparison comparisonType) { Debug.Assert((uint)comparisonType <= (uint)StringComparison.OrdinalIgnoreCase); // Culture enums can be & with CompareOptions.IgnoreCase 0x01 to extract if IgnoreCase or CompareOptions.None 0x00 // // CompareOptions.None 0x00 // CompareOptions.IgnoreCase 0x01 // // StringComparison.CurrentCulture: 0x00 // StringComparison.InvariantCulture: 0x02 // StringComparison.Ordinal 0x04 // // StringComparison.CurrentCultureIgnoreCase: 0x01 // StringComparison.InvariantCultureIgnoreCase: 0x03 // StringComparison.OrdinalIgnoreCase 0x05 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 } } }