use IntPtr instead of CriticalHandle to avoid resurrection issues. It's ok to never...
[mono-project.git] / netcore / System.Private.CoreLib / shared / System / Globalization / CompareInfo.Windows.cs
bloba1c4a364fcbe698cf7dd81ce975b4febbe268cef
1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3 // See the LICENSE file in the project root for more information.
5 using System.Buffers;
6 using System.Diagnostics;
7 using System.Runtime.InteropServices;
9 using Internal.Runtime.CompilerServices;
11 namespace System.Globalization
13 public partial class CompareInfo
15 private unsafe void InitSort(CultureInfo culture)
17 _sortName = culture.SortName;
19 if (GlobalizationMode.Invariant)
21 _sortHandle = IntPtr.Zero;
23 else
25 const uint LCMAP_SORTHANDLE = 0x20000000;
27 IntPtr handle;
28 int ret = Interop.Kernel32.LCMapStringEx(_sortName, LCMAP_SORTHANDLE, null, 0, &handle, IntPtr.Size, null, null, IntPtr.Zero);
29 _sortHandle = ret > 0 ? handle : IntPtr.Zero;
33 private static unsafe int FindStringOrdinal(
34 uint dwFindStringOrdinalFlags,
35 string stringSource,
36 int offset,
37 int cchSource,
38 string value,
39 int cchValue,
40 bool bIgnoreCase)
42 Debug.Assert(!GlobalizationMode.Invariant);
43 Debug.Assert(stringSource != null);
44 Debug.Assert(value != null);
46 fixed (char* pSource = stringSource)
47 fixed (char* pValue = value)
49 int ret = Interop.Kernel32.FindStringOrdinal(
50 dwFindStringOrdinalFlags,
51 pSource + offset,
52 cchSource,
53 pValue,
54 cchValue,
55 bIgnoreCase ? 1 : 0);
56 return ret < 0 ? ret : ret + offset;
60 private static unsafe int FindStringOrdinal(
61 uint dwFindStringOrdinalFlags,
62 ReadOnlySpan<char> source,
63 ReadOnlySpan<char> value,
64 bool bIgnoreCase)
66 Debug.Assert(!GlobalizationMode.Invariant);
67 Debug.Assert(!source.IsEmpty);
68 Debug.Assert(!value.IsEmpty);
70 fixed (char* pSource = &MemoryMarshal.GetReference(source))
71 fixed (char* pValue = &MemoryMarshal.GetReference(value))
73 int ret = Interop.Kernel32.FindStringOrdinal(
74 dwFindStringOrdinalFlags,
75 pSource,
76 source.Length,
77 pValue,
78 value.Length,
79 bIgnoreCase ? 1 : 0);
80 return ret;
84 internal static int IndexOfOrdinalCore(string source, string value, int startIndex, int count, bool ignoreCase)
86 Debug.Assert(!GlobalizationMode.Invariant);
88 Debug.Assert(source != null);
89 Debug.Assert(value != null);
91 return FindStringOrdinal(FIND_FROMSTART, source, startIndex, count, value, value.Length, ignoreCase);
94 internal static int IndexOfOrdinalCore(ReadOnlySpan<char> source, ReadOnlySpan<char> value, bool ignoreCase, bool fromBeginning)
96 Debug.Assert(!GlobalizationMode.Invariant);
98 Debug.Assert(source.Length != 0);
99 Debug.Assert(value.Length != 0);
101 uint positionFlag = fromBeginning ? (uint)FIND_FROMSTART : FIND_FROMEND;
102 return FindStringOrdinal(positionFlag, source, value, ignoreCase);
105 internal static int LastIndexOfOrdinalCore(string source, string value, int startIndex, int count, bool ignoreCase)
107 Debug.Assert(!GlobalizationMode.Invariant);
109 Debug.Assert(source != null);
110 Debug.Assert(value != null);
112 return FindStringOrdinal(FIND_FROMEND, source, startIndex - count + 1, count, value, value.Length, ignoreCase);
115 private unsafe int GetHashCodeOfStringCore(ReadOnlySpan<char> source, CompareOptions options)
117 Debug.Assert(!GlobalizationMode.Invariant);
118 Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0);
120 if (source.Length == 0)
122 return 0;
125 uint flags = LCMAP_SORTKEY | (uint)GetNativeCompareFlags(options);
127 fixed (char* pSource = source)
129 int sortKeyLength = Interop.Kernel32.LCMapStringEx(_sortHandle != IntPtr.Zero ? null : _sortName,
130 flags,
131 pSource, source.Length /* in chars */,
132 null, 0,
133 null, null, _sortHandle);
134 if (sortKeyLength == 0)
136 throw new ArgumentException(SR.Arg_ExternalException);
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.
144 byte[]? borrowedArr = null;
145 Span<byte> span = sortKeyLength <= 512 ?
146 stackalloc byte[512] :
147 (borrowedArr = ArrayPool<byte>.Shared.Rent(sortKeyLength));
149 fixed (byte* pSortKey = &MemoryMarshal.GetReference(span))
151 if (Interop.Kernel32.LCMapStringEx(_sortHandle != IntPtr.Zero ? null : _sortName,
152 flags,
153 pSource, source.Length /* in chars */,
154 pSortKey, sortKeyLength,
155 null, null, _sortHandle) != sortKeyLength)
157 throw new ArgumentException(SR.Arg_ExternalException);
161 int hash = Marvin.ComputeHash32(span.Slice(0, sortKeyLength), Marvin.DefaultSeed);
163 // Return the borrowed array if necessary.
164 if (borrowedArr != null)
166 ArrayPool<byte>.Shared.Return(borrowedArr);
169 return hash;
173 private static unsafe int CompareStringOrdinalIgnoreCase(ref char string1, int count1, ref char string2, int count2)
175 Debug.Assert(!GlobalizationMode.Invariant);
177 fixed (char* char1 = &string1)
178 fixed (char* char2 = &string2)
180 // Use the OS to compare and then convert the result to expected value by subtracting 2
181 return Interop.Kernel32.CompareStringOrdinal(char1, count1, char2, count2, true) - 2;
185 // TODO https://github.com/dotnet/coreclr/issues/13827:
186 // This method shouldn't be necessary, as we should be able to just use the overload
187 // that takes two spans. But due to this issue, that's adding significant overhead.
188 private unsafe int CompareString(ReadOnlySpan<char> string1, string string2, CompareOptions options)
190 Debug.Assert(string2 != null);
191 Debug.Assert(!GlobalizationMode.Invariant);
192 Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0);
194 string? localeName = _sortHandle != IntPtr.Zero ? null : _sortName;
196 fixed (char* pLocaleName = localeName)
197 fixed (char* pString1 = &MemoryMarshal.GetReference(string1))
198 fixed (char* pString2 = &string2.GetRawStringData())
200 Debug.Assert(pString1 != null);
201 int result = Interop.Kernel32.CompareStringEx(
202 pLocaleName,
203 (uint)GetNativeCompareFlags(options),
204 pString1,
205 string1.Length,
206 pString2,
207 string2.Length,
208 null,
209 null,
210 _sortHandle);
212 if (result == 0)
214 throw new ArgumentException(SR.Arg_ExternalException);
217 // Map CompareStringEx return value to -1, 0, 1.
218 return result - 2;
222 private unsafe int CompareString(ReadOnlySpan<char> string1, ReadOnlySpan<char> string2, CompareOptions options)
224 Debug.Assert(!GlobalizationMode.Invariant);
225 Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0);
227 string? localeName = _sortHandle != IntPtr.Zero ? null : _sortName;
229 fixed (char* pLocaleName = localeName)
230 fixed (char* pString1 = &MemoryMarshal.GetReference(string1))
231 fixed (char* pString2 = &MemoryMarshal.GetReference(string2))
233 Debug.Assert(pString1 != null);
234 Debug.Assert(pString2 != null);
235 int result = Interop.Kernel32.CompareStringEx(
236 pLocaleName,
237 (uint)GetNativeCompareFlags(options),
238 pString1,
239 string1.Length,
240 pString2,
241 string2.Length,
242 null,
243 null,
244 _sortHandle);
246 if (result == 0)
248 throw new ArgumentException(SR.Arg_ExternalException);
251 // Map CompareStringEx return value to -1, 0, 1.
252 return result - 2;
256 private unsafe int FindString(
257 uint dwFindNLSStringFlags,
258 ReadOnlySpan<char> lpStringSource,
259 ReadOnlySpan<char> lpStringValue,
260 int* pcchFound)
262 Debug.Assert(!GlobalizationMode.Invariant);
263 Debug.Assert(!lpStringSource.IsEmpty);
264 Debug.Assert(!lpStringValue.IsEmpty);
266 string? localeName = _sortHandle != IntPtr.Zero ? null : _sortName;
268 fixed (char* pLocaleName = localeName)
269 fixed (char* pSource = &MemoryMarshal.GetReference(lpStringSource))
270 fixed (char* pValue = &MemoryMarshal.GetReference(lpStringValue))
272 return Interop.Kernel32.FindNLSStringEx(
273 pLocaleName,
274 dwFindNLSStringFlags,
275 pSource,
276 lpStringSource.Length,
277 pValue,
278 lpStringValue.Length,
279 pcchFound,
280 null,
281 null,
282 _sortHandle);
286 private unsafe int FindString(
287 uint dwFindNLSStringFlags,
288 string lpStringSource,
289 int startSource,
290 int cchSource,
291 string lpStringValue,
292 int startValue,
293 int cchValue,
294 int* pcchFound)
296 Debug.Assert(!GlobalizationMode.Invariant);
297 Debug.Assert(lpStringSource != null);
298 Debug.Assert(lpStringValue != null);
300 string? localeName = _sortHandle != IntPtr.Zero ? null : _sortName;
302 fixed (char* pLocaleName = localeName)
303 fixed (char* pSource = lpStringSource)
304 fixed (char* pValue = lpStringValue)
306 char* pS = pSource + startSource;
307 char* pV = pValue + startValue;
309 return Interop.Kernel32.FindNLSStringEx(
310 pLocaleName,
311 dwFindNLSStringFlags,
313 cchSource,
315 cchValue,
316 pcchFound,
317 null,
318 null,
319 _sortHandle);
323 internal unsafe int IndexOfCore(string source, string target, int startIndex, int count, CompareOptions options, int* matchLengthPtr)
325 Debug.Assert(!GlobalizationMode.Invariant);
327 Debug.Assert(source != null);
328 Debug.Assert(target != null);
329 Debug.Assert((options & CompareOptions.OrdinalIgnoreCase) == 0);
330 Debug.Assert((options & CompareOptions.Ordinal) == 0);
332 int retValue = FindString(FIND_FROMSTART | (uint)GetNativeCompareFlags(options), source, startIndex, count,
333 target, 0, target.Length, matchLengthPtr);
334 if (retValue >= 0)
336 return retValue + startIndex;
339 return -1;
342 internal unsafe int IndexOfCore(ReadOnlySpan<char> source, ReadOnlySpan<char> target, CompareOptions options, int* matchLengthPtr, bool fromBeginning)
344 Debug.Assert(!GlobalizationMode.Invariant);
346 Debug.Assert(source.Length != 0);
347 Debug.Assert(target.Length != 0);
348 Debug.Assert((options == CompareOptions.None || options == CompareOptions.IgnoreCase));
350 uint positionFlag = fromBeginning ? (uint)FIND_FROMSTART : FIND_FROMEND;
351 return FindString(positionFlag | (uint)GetNativeCompareFlags(options), source, target, matchLengthPtr);
354 private unsafe int LastIndexOfCore(string source, string target, int startIndex, int count, CompareOptions options)
356 Debug.Assert(!GlobalizationMode.Invariant);
358 Debug.Assert(!string.IsNullOrEmpty(source));
359 Debug.Assert(target != null);
360 Debug.Assert((options & CompareOptions.OrdinalIgnoreCase) == 0);
362 if (target.Length == 0)
363 return startIndex;
365 if ((options & CompareOptions.Ordinal) != 0)
367 return FastLastIndexOfString(source, target, startIndex, count, target.Length);
369 else
371 int retValue = FindString(FIND_FROMEND | (uint)GetNativeCompareFlags(options), source, startIndex - count + 1,
372 count, target, 0, target.Length, null);
374 if (retValue >= 0)
376 return retValue + startIndex - (count - 1);
380 return -1;
383 private unsafe bool StartsWith(string source, string prefix, CompareOptions options)
385 Debug.Assert(!GlobalizationMode.Invariant);
387 Debug.Assert(!string.IsNullOrEmpty(source));
388 Debug.Assert(!string.IsNullOrEmpty(prefix));
389 Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0);
391 return FindString(FIND_STARTSWITH | (uint)GetNativeCompareFlags(options), source, 0, source.Length,
392 prefix, 0, prefix.Length, null) >= 0;
395 private unsafe bool StartsWith(ReadOnlySpan<char> source, ReadOnlySpan<char> prefix, CompareOptions options)
397 Debug.Assert(!GlobalizationMode.Invariant);
399 Debug.Assert(!source.IsEmpty);
400 Debug.Assert(!prefix.IsEmpty);
401 Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0);
403 return FindString(FIND_STARTSWITH | (uint)GetNativeCompareFlags(options), source, prefix, null) >= 0;
406 private unsafe bool EndsWith(string source, string suffix, CompareOptions options)
408 Debug.Assert(!GlobalizationMode.Invariant);
410 Debug.Assert(!string.IsNullOrEmpty(source));
411 Debug.Assert(!string.IsNullOrEmpty(suffix));
412 Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0);
414 return FindString(FIND_ENDSWITH | (uint)GetNativeCompareFlags(options), source, 0, source.Length,
415 suffix, 0, suffix.Length, null) >= 0;
418 private unsafe bool EndsWith(ReadOnlySpan<char> source, ReadOnlySpan<char> suffix, CompareOptions options)
420 Debug.Assert(!GlobalizationMode.Invariant);
422 Debug.Assert(!source.IsEmpty);
423 Debug.Assert(!suffix.IsEmpty);
424 Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0);
426 return FindString(FIND_ENDSWITH | (uint)GetNativeCompareFlags(options), source, suffix, null) >= 0;
429 // PAL ends here
430 private const uint LCMAP_SORTKEY = 0x00000400;
431 private const uint LCMAP_HASH = 0x00040000;
433 private const int FIND_STARTSWITH = 0x00100000;
434 private const int FIND_ENDSWITH = 0x00200000;
435 private const int FIND_FROMSTART = 0x00400000;
436 private const int FIND_FROMEND = 0x00800000;
438 // TODO: Instead of this method could we just have upstack code call LastIndexOfOrdinal with ignoreCase = false?
439 private static unsafe int FastLastIndexOfString(string source, string target, int startIndex, int sourceCount, int targetCount)
441 int retValue = -1;
443 int sourceStartIndex = startIndex - sourceCount + 1;
445 fixed (char* pSource = source, spTarget = target)
447 char* spSubSource = pSource + sourceStartIndex;
449 int endPattern = sourceCount - targetCount;
450 if (endPattern < 0)
451 return -1;
453 Debug.Assert(target.Length >= 1);
454 char patternChar0 = spTarget[0];
455 for (int ctrSrc = endPattern; ctrSrc >= 0; ctrSrc--)
457 if (spSubSource[ctrSrc] != patternChar0)
458 continue;
460 int ctrPat;
461 for (ctrPat = 1; ctrPat < targetCount; ctrPat++)
463 if (spSubSource[ctrSrc + ctrPat] != spTarget[ctrPat])
464 break;
466 if (ctrPat == targetCount)
468 retValue = ctrSrc;
469 break;
473 if (retValue >= 0)
475 retValue += startIndex - sourceCount + 1;
479 return retValue;
482 private unsafe SortKey CreateSortKey(string source, CompareOptions options)
484 Debug.Assert(!GlobalizationMode.Invariant);
486 if (source == null) { throw new ArgumentNullException(nameof(source)); }
488 if ((options & ValidSortkeyCtorMaskOffFlags) != 0)
490 throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
493 byte [] keyData;
494 if (source.Length == 0)
496 keyData = Array.Empty<byte>();
498 else
500 uint flags = LCMAP_SORTKEY | (uint)GetNativeCompareFlags(options);
502 fixed (char *pSource = source)
504 int sortKeyLength = Interop.Kernel32.LCMapStringEx(_sortHandle != IntPtr.Zero ? null : _sortName,
505 flags,
506 pSource, source.Length,
507 null, 0,
508 null, null, _sortHandle);
509 if (sortKeyLength == 0)
511 throw new ArgumentException(SR.Arg_ExternalException);
514 keyData = new byte[sortKeyLength];
516 fixed (byte* pBytes = keyData)
518 if (Interop.Kernel32.LCMapStringEx(_sortHandle != IntPtr.Zero ? null : _sortName,
519 flags,
520 pSource, source.Length,
521 pBytes, keyData.Length,
522 null, null, _sortHandle) != sortKeyLength)
524 throw new ArgumentException(SR.Arg_ExternalException);
530 return new SortKey(Name, source, options, keyData);
533 private static unsafe bool IsSortable(char* text, int length)
535 Debug.Assert(!GlobalizationMode.Invariant);
536 Debug.Assert(text != null);
538 return Interop.Kernel32.IsNLSDefinedString(Interop.Kernel32.COMPARE_STRING, 0, IntPtr.Zero, text, length);
541 private const int COMPARE_OPTIONS_ORDINAL = 0x40000000; // Ordinal
542 private const int NORM_IGNORECASE = 0x00000001; // Ignores case. (use LINGUISTIC_IGNORECASE instead)
543 private const int NORM_IGNOREKANATYPE = 0x00010000; // Does not differentiate between Hiragana and Katakana characters. Corresponding Hiragana and Katakana will compare as equal.
544 private const int NORM_IGNORENONSPACE = 0x00000002; // Ignores nonspacing. This flag also removes Japanese accent characters. (use LINGUISTIC_IGNOREDIACRITIC instead)
545 private const int NORM_IGNORESYMBOLS = 0x00000004; // Ignores symbols.
546 private const int NORM_IGNOREWIDTH = 0x00020000; // Does not differentiate between a single-byte character and the same character as a double-byte character.
547 private const int NORM_LINGUISTIC_CASING = 0x08000000; // use linguistic rules for casing
548 private const int SORT_STRINGSORT = 0x00001000; // Treats punctuation the same as symbols.
550 private static int GetNativeCompareFlags(CompareOptions options)
552 // Use "linguistic casing" by default (load the culture's casing exception tables)
553 int nativeCompareFlags = NORM_LINGUISTIC_CASING;
555 if ((options & CompareOptions.IgnoreCase) != 0) { nativeCompareFlags |= NORM_IGNORECASE; }
556 if ((options & CompareOptions.IgnoreKanaType) != 0) { nativeCompareFlags |= NORM_IGNOREKANATYPE; }
557 if ((options & CompareOptions.IgnoreNonSpace) != 0) { nativeCompareFlags |= NORM_IGNORENONSPACE; }
558 if ((options & CompareOptions.IgnoreSymbols) != 0) { nativeCompareFlags |= NORM_IGNORESYMBOLS; }
559 if ((options & CompareOptions.IgnoreWidth) != 0) { nativeCompareFlags |= NORM_IGNOREWIDTH; }
560 if ((options & CompareOptions.StringSort) != 0) { nativeCompareFlags |= SORT_STRINGSORT; }
562 // TODO: Can we try for GetNativeCompareFlags to never
563 // take Ordinal or OrdinalIgnoreCase. This value is not part of Win32, we just handle it special
564 // in some places.
565 // Suffix & Prefix shouldn't use this, make sure to turn off the NORM_LINGUISTIC_CASING flag
566 if (options == CompareOptions.Ordinal) { nativeCompareFlags = COMPARE_OPTIONS_ORDINAL; }
568 Debug.Assert(((options & ~(CompareOptions.IgnoreCase |
569 CompareOptions.IgnoreKanaType |
570 CompareOptions.IgnoreNonSpace |
571 CompareOptions.IgnoreSymbols |
572 CompareOptions.IgnoreWidth |
573 CompareOptions.StringSort)) == 0) ||
574 (options == CompareOptions.Ordinal), "[CompareInfo.GetNativeCompareFlags]Expected all flags to be handled");
576 return nativeCompareFlags;
579 private unsafe SortVersion GetSortVersion()
581 Debug.Assert(!GlobalizationMode.Invariant);
583 Interop.Kernel32.NlsVersionInfoEx nlsVersion = new Interop.Kernel32.NlsVersionInfoEx();
584 nlsVersion.dwNLSVersionInfoSize = sizeof(Interop.Kernel32.NlsVersionInfoEx);
585 Interop.Kernel32.GetNLSVersionEx(Interop.Kernel32.COMPARE_STRING, _sortName, &nlsVersion);
586 return new SortVersion(
587 nlsVersion.dwNLSVersion,
588 nlsVersion.dwEffectiveId == 0 ? LCID : nlsVersion.dwEffectiveId,
589 nlsVersion.guidCustomVersion);