Skip to content

Commit a940030

Browse files
Add string.GetHashCode(ROS<char>) and related APIs (dotnet#20422)
1 parent b30280d commit a940030

File tree

5 files changed

+133
-57
lines changed

5 files changed

+133
-57
lines changed

src/System.Private.CoreLib/shared/Interop/Unix/System.Globalization.Native/Interop.Collation.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ internal static partial class Globalization
4848
internal static extern unsafe bool EndsWith(SafeSortHandle sortHandle, string target, int cwTargetLength, string source, int cwSourceLength, CompareOptions options);
4949

5050
[DllImport(Libraries.GlobalizationNative, CharSet = CharSet.Unicode, EntryPoint = "GlobalizationNative_GetSortKey")]
51-
internal static extern unsafe int GetSortKey(SafeSortHandle sortHandle, string str, int strLength, byte* sortKey, int sortKeyLength, CompareOptions options);
51+
internal static extern unsafe int GetSortKey(SafeSortHandle sortHandle, char* str, int strLength, byte* sortKey, int sortKeyLength, CompareOptions options);
5252

5353
[DllImport(Libraries.GlobalizationNative, CharSet = CharSet.Unicode, EntryPoint = "GlobalizationNative_CompareStringOrdinalIgnoreCase")]
5454
internal static extern unsafe int CompareStringOrdinalIgnoreCase(char* lpStr1, int cwStr1Len, char* lpStr2, int cwStr2Len);

src/System.Private.CoreLib/shared/System/Globalization/CompareInfo.Unix.cs

+30-26
Original file line numberDiff line numberDiff line change
@@ -798,14 +798,17 @@ private unsafe SortKey CreateSortKey(string source, CompareOptions options)
798798
}
799799
else
800800
{
801-
int sortKeyLength = Interop.Globalization.GetSortKey(_sortHandle, source, source.Length, null, 0, options);
802-
keyData = new byte[sortKeyLength];
803-
804-
fixed (byte* pSortKey = keyData)
801+
fixed (char* pSource = source)
805802
{
806-
if (Interop.Globalization.GetSortKey(_sortHandle, source, source.Length, pSortKey, sortKeyLength, options) != sortKeyLength)
803+
int sortKeyLength = Interop.Globalization.GetSortKey(_sortHandle, pSource, source.Length, null, 0, options);
804+
keyData = new byte[sortKeyLength];
805+
806+
fixed (byte* pSortKey = keyData)
807807
{
808-
throw new ArgumentException(SR.Arg_ExternalException);
808+
if (Interop.Globalization.GetSortKey(_sortHandle, pSource, source.Length, pSortKey, sortKeyLength, options) != sortKeyLength)
809+
{
810+
throw new ArgumentException(SR.Arg_ExternalException);
811+
}
809812
}
810813
}
811814
}
@@ -856,42 +859,43 @@ private static unsafe bool IsSortable(char *text, int length)
856859
// ---- PAL layer ends here ----
857860
// -----------------------------
858861

859-
internal unsafe int GetHashCodeOfStringCore(string source, CompareOptions options)
862+
internal unsafe int GetHashCodeOfStringCore(ReadOnlySpan<char> source, CompareOptions options)
860863
{
861864
Debug.Assert(!_invariantMode);
862-
863-
Debug.Assert(source != null);
864865
Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0);
865866

866867
if (source.Length == 0)
867868
{
868869
return 0;
869870
}
870871

871-
int sortKeyLength = Interop.Globalization.GetSortKey(_sortHandle, source, source.Length, null, 0, options);
872+
fixed (char* pSource = source)
873+
{
874+
int sortKeyLength = Interop.Globalization.GetSortKey(_sortHandle, pSource, source.Length, null, 0, options);
872875

873-
byte[] borrowedArr = null;
874-
Span<byte> span = sortKeyLength <= 512 ?
875-
stackalloc byte[512] :
876-
(borrowedArr = ArrayPool<byte>.Shared.Rent(sortKeyLength));
876+
byte[] borrowedArr = null;
877+
Span<byte> span = sortKeyLength <= 512 ?
878+
stackalloc byte[512] :
879+
(borrowedArr = ArrayPool<byte>.Shared.Rent(sortKeyLength));
877880

878-
fixed (byte* pSortKey = &MemoryMarshal.GetReference(span))
879-
{
880-
if (Interop.Globalization.GetSortKey(_sortHandle, source, source.Length, pSortKey, sortKeyLength, options) != sortKeyLength)
881+
fixed (byte* pSortKey = &MemoryMarshal.GetReference(span))
881882
{
882-
throw new ArgumentException(SR.Arg_ExternalException);
883+
if (Interop.Globalization.GetSortKey(_sortHandle, pSource, source.Length, pSortKey, sortKeyLength, options) != sortKeyLength)
884+
{
885+
throw new ArgumentException(SR.Arg_ExternalException);
886+
}
883887
}
884-
}
885888

886-
int hash = Marvin.ComputeHash32(span.Slice(0, sortKeyLength), Marvin.DefaultSeed);
889+
int hash = Marvin.ComputeHash32(span.Slice(0, sortKeyLength), Marvin.DefaultSeed);
887890

888-
// Return the borrowed array if necessary.
889-
if (borrowedArr != null)
890-
{
891-
ArrayPool<byte>.Shared.Return(borrowedArr);
892-
}
891+
// Return the borrowed array if necessary.
892+
if (borrowedArr != null)
893+
{
894+
ArrayPool<byte>.Shared.Return(borrowedArr);
895+
}
893896

894-
return hash;
897+
return hash;
898+
}
895899
}
896900

897901
private static CompareOptions GetOrdinalCompareOptions(CompareOptions options)

src/System.Private.CoreLib/shared/System/Globalization/CompareInfo.Windows.cs

+9-6
Original file line numberDiff line numberDiff line change
@@ -111,12 +111,10 @@ internal static int LastIndexOfOrdinalCore(string source, string value, int star
111111

112112
return FindStringOrdinal(FIND_FROMEND, source, startIndex - count + 1, count, value, value.Length, ignoreCase);
113113
}
114-
115-
private unsafe int GetHashCodeOfStringCore(string source, CompareOptions options)
114+
115+
private unsafe int GetHashCodeOfStringCore(ReadOnlySpan<char> source, CompareOptions options)
116116
{
117117
Debug.Assert(!_invariantMode);
118-
119-
Debug.Assert(source != null);
120118
Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0);
121119

122120
if (source.Length == 0)
@@ -130,14 +128,19 @@ private unsafe int GetHashCodeOfStringCore(string source, CompareOptions options
130128
{
131129
int sortKeyLength = Interop.Kernel32.LCMapStringEx(_sortHandle != IntPtr.Zero ? null : _sortName,
132130
flags,
133-
pSource, source.Length,
131+
pSource, source.Length /* in chars */,
134132
null, 0,
135133
null, null, _sortHandle);
136134
if (sortKeyLength == 0)
137135
{
138136
throw new ArgumentException(SR.Arg_ExternalException);
139137
}
140138

139+
// Note in calls to LCMapStringEx below, the input buffer is specified in wchars (and wchar count),
140+
// but the output buffer is specified in bytes (and byte count). This is because when generating
141+
// sort keys, LCMapStringEx treats the output buffer as containing opaque binary data.
142+
// See https://docs.microsoft.com/en-us/windows/desktop/api/winnls/nf-winnls-lcmapstringex.
143+
141144
byte[] borrowedArr = null;
142145
Span<byte> span = sortKeyLength <= 512 ?
143146
stackalloc byte[512] :
@@ -147,7 +150,7 @@ private unsafe int GetHashCodeOfStringCore(string source, CompareOptions options
147150
{
148151
if (Interop.Kernel32.LCMapStringEx(_sortHandle != IntPtr.Zero ? null : _sortName,
149152
flags,
150-
pSource, source.Length,
153+
pSource, source.Length /* in chars */,
151154
pSortKey, sortKeyLength,
152155
null, null, _sortHandle) != sortKeyLength)
153156
{

src/System.Private.CoreLib/shared/System/Globalization/CompareInfo.cs

+50-22
Original file line numberDiff line numberDiff line change
@@ -1420,43 +1420,71 @@ internal int GetHashCodeOfString(string source, CompareOptions options)
14201420
{
14211421
throw new ArgumentNullException(nameof(source));
14221422
}
1423+
if ((options & ValidHashCodeOfStringMaskOffFlags) == 0)
1424+
{
1425+
// No unsupported flags are set - continue on with the regular logic
1426+
1427+
if (_invariantMode)
1428+
{
1429+
return ((options & CompareOptions.IgnoreCase) != 0) ? source.GetHashCodeOrdinalIgnoreCase() : source.GetHashCode();
1430+
}
14231431

1424-
if ((options & ValidHashCodeOfStringMaskOffFlags) != 0)
1432+
return GetHashCodeOfStringCore(source, options);
1433+
}
1434+
else if (options == CompareOptions.Ordinal)
14251435
{
1426-
throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
1436+
// We allow Ordinal in isolation
1437+
return source.GetHashCode();
14271438
}
1428-
1429-
if (_invariantMode)
1439+
else if (options == CompareOptions.OrdinalIgnoreCase)
14301440
{
1431-
return ((options & CompareOptions.IgnoreCase) != 0) ? source.GetHashCodeOrdinalIgnoreCase() : source.GetHashCode();
1441+
// We allow OrdinalIgnoreCase in isolation
1442+
return source.GetHashCodeOrdinalIgnoreCase();
1443+
}
1444+
else
1445+
{
1446+
// Unsupported combination of flags specified
1447+
throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
14321448
}
1433-
1434-
return GetHashCodeOfStringCore(source, options);
14351449
}
14361450

14371451
public virtual int GetHashCode(string source, CompareOptions options)
14381452
{
1439-
if (source == null)
1453+
// virtual method delegates to non-virtual method
1454+
return GetHashCodeOfString(source, options);
1455+
}
1456+
1457+
public int GetHashCode(ReadOnlySpan<char> source, CompareOptions options)
1458+
{
1459+
//
1460+
// Parameter validation
1461+
//
1462+
if ((options & ValidHashCodeOfStringMaskOffFlags) == 0)
14401463
{
1441-
throw new ArgumentNullException(nameof(source));
1442-
}
1464+
// No unsupported flags are set - continue on with the regular logic
14431465

1444-
if (options == CompareOptions.Ordinal)
1466+
if (_invariantMode)
1467+
{
1468+
return ((options & CompareOptions.IgnoreCase) != 0) ? string.GetHashCodeOrdinalIgnoreCase(source) : string.GetHashCode(source);
1469+
}
1470+
1471+
return GetHashCodeOfStringCore(source, options);
1472+
}
1473+
else if (options == CompareOptions.Ordinal)
14451474
{
1446-
return source.GetHashCode();
1475+
// We allow Ordinal in isolation
1476+
return string.GetHashCode(source);
14471477
}
1448-
1449-
if (options == CompareOptions.OrdinalIgnoreCase)
1478+
else if (options == CompareOptions.OrdinalIgnoreCase)
14501479
{
1451-
return source.GetHashCodeOrdinalIgnoreCase();
1480+
// We allow OrdinalIgnoreCase in isolation
1481+
return string.GetHashCodeOrdinalIgnoreCase(source);
1482+
}
1483+
else
1484+
{
1485+
// Unsupported combination of flags specified
1486+
throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
14521487
}
1453-
1454-
//
1455-
// GetHashCodeOfString does more parameters validation. basically will throw when
1456-
// having Ordinal, OrdinalIgnoreCase and StringSort
1457-
//
1458-
1459-
return GetHashCodeOfString(source, options);
14601488
}
14611489

14621490
////////////////////////////////////////////////////////////////////////

src/System.Private.CoreLib/shared/System/String.Comparison.cs

+43-2
Original file line numberDiff line numberDiff line change
@@ -748,7 +748,7 @@ public static bool Equals(string a, string b, StringComparison comparisonType)
748748
public override int GetHashCode()
749749
{
750750
ulong seed = Marvin.DefaultSeed;
751-
return Marvin.ComputeHash32(ref Unsafe.As<char, byte>(ref _firstChar), _stringLength * 2, (uint)seed, (uint)(seed >> 32));
751+
return Marvin.ComputeHash32(ref Unsafe.As<char, byte>(ref _firstChar), _stringLength * 2 /* in bytes, not chars */, (uint)seed, (uint)(seed >> 32));
752752
}
753753

754754
// Gets a hash code for this string and this comparison. If strings A and B and comparison C are such
@@ -759,7 +759,48 @@ public override int GetHashCode()
759759
internal int GetHashCodeOrdinalIgnoreCase()
760760
{
761761
ulong seed = Marvin.DefaultSeed;
762-
return Marvin.ComputeHash32OrdinalIgnoreCase(ref _firstChar, _stringLength, (uint)seed, (uint)(seed >> 32));
762+
return Marvin.ComputeHash32OrdinalIgnoreCase(ref _firstChar, _stringLength /* in chars, not bytes */, (uint)seed, (uint)(seed >> 32));
763+
}
764+
765+
// A span-based equivalent of String.GetHashCode(). Computes an ordinal hash code.
766+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
767+
public static int GetHashCode(ReadOnlySpan<char> value)
768+
{
769+
ulong seed = Marvin.DefaultSeed;
770+
return Marvin.ComputeHash32(ref Unsafe.As<char, byte>(ref MemoryMarshal.GetReference(value)), value.Length * 2 /* in bytes, not chars */, (uint)seed, (uint)(seed >> 32));
771+
}
772+
773+
// A span-based equivalent of String.GetHashCode(StringComparison). Uses the specified comparison type.
774+
public static int GetHashCode(ReadOnlySpan<char> value, StringComparison comparisonType)
775+
{
776+
switch (comparisonType)
777+
{
778+
case StringComparison.CurrentCulture:
779+
case StringComparison.CurrentCultureIgnoreCase:
780+
return CultureInfo.CurrentCulture.CompareInfo.GetHashCode(value, GetCaseCompareOfComparisonCulture(comparisonType));
781+
782+
case StringComparison.InvariantCulture:
783+
case StringComparison.InvariantCultureIgnoreCase:
784+
return CultureInfo.InvariantCulture.CompareInfo.GetHashCode(value, GetCaseCompareOfComparisonCulture(comparisonType));
785+
786+
case StringComparison.Ordinal:
787+
return GetHashCode(value);
788+
789+
case StringComparison.OrdinalIgnoreCase:
790+
return GetHashCodeOrdinalIgnoreCase(value);
791+
792+
default:
793+
ThrowHelper.ThrowArgumentException(ExceptionResource.NotSupported_StringComparison, ExceptionArgument.comparisonType);
794+
Debug.Fail("Should not reach this point.");
795+
return default;
796+
}
797+
}
798+
799+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
800+
internal static int GetHashCodeOrdinalIgnoreCase(ReadOnlySpan<char> value)
801+
{
802+
ulong seed = Marvin.DefaultSeed;
803+
return Marvin.ComputeHash32OrdinalIgnoreCase(ref MemoryMarshal.GetReference(value), value.Length /* in chars, not bytes */, (uint)seed, (uint)(seed >> 32));
763804
}
764805

765806
// Use this if and only if 'Denial of Service' attacks are not a concern (i.e. never used for free-form user input),

0 commit comments

Comments
 (0)