File: src\libraries\System.Private.CoreLib\src\System\Globalization\CompareInfo.Nls.cs
Web Access
Project: src\src\coreclr\System.Private.CoreLib\System.Private.CoreLib.csproj (System.Private.CoreLib)
// 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.Diagnostics;
using System.Runtime.InteropServices;
 
namespace System.Globalization
{
    public partial class CompareInfo
    {
        private void NlsInitSortHandle()
        {
            Debug.Assert(GlobalizationMode.UseNls);
            _sortHandle = NlsGetSortHandle(_sortName);
        }
 
        internal static unsafe IntPtr NlsGetSortHandle(string cultureName)
        {
            if (GlobalizationMode.Invariant)
            {
                return IntPtr.Zero;
            }
 
            IntPtr handle;
            int ret = Interop.Kernel32.LCMapStringEx(cultureName, Interop.Kernel32.LCMAP_SORTHANDLE, null, 0, &handle, IntPtr.Size, null, null, IntPtr.Zero);
            if (ret > 0)
            {
                // Even if we can get the sort handle, it is not guaranteed to work when Windows compatibility shim is applied
                // e.g. Windows 7 compatibility mode. We need to ensure it is working before using it.
                // otherwise the whole framework app will not start.
                int hashValue = 0;
                char a = 'a';
                ret = Interop.Kernel32.LCMapStringEx(null, Interop.Kernel32.LCMAP_HASH, &a, 1, &hashValue, sizeof(int), null, null, handle);
                if (ret > 1)
                {
                    return handle;
                }
            }
 
            return IntPtr.Zero;
        }
 
        private static unsafe int FindStringOrdinal(
            uint dwFindStringOrdinalFlags,
            ReadOnlySpan<char> source,
            ReadOnlySpan<char> value,
            bool bIgnoreCase)
        {
            Debug.Assert(!GlobalizationMode.Invariant);
            Debug.Assert(!source.IsEmpty);
            Debug.Assert(!value.IsEmpty);
 
            fixed (char* pSource = &MemoryMarshal.GetReference(source))
            fixed (char* pValue = &MemoryMarshal.GetReference(value))
            {
                Debug.Assert(pSource != null);
                Debug.Assert(pValue != null);
 
                int ret = Interop.Kernel32.FindStringOrdinal(
                            dwFindStringOrdinalFlags,
                            pSource,
                            source.Length,
                            pValue,
                            value.Length,
                            bIgnoreCase ? Interop.BOOL.TRUE : Interop.BOOL.FALSE);
 
                Debug.Assert(ret >= -1 && ret <= source.Length);
 
                // SetLastError is only performed under debug builds.
                Debug.Assert(ret >= 0 || Marshal.GetLastPInvokeError() == Interop.Errors.ERROR_SUCCESS);
 
                return ret;
            }
        }
 
        internal static int NlsIndexOfOrdinalCore(ReadOnlySpan<char> source, ReadOnlySpan<char> value, bool ignoreCase, bool fromBeginning)
        {
            Debug.Assert(!GlobalizationMode.Invariant);
            Debug.Assert(GlobalizationMode.UseNls);
 
            Debug.Assert(source.Length != 0);
            Debug.Assert(value.Length != 0);
 
            uint positionFlag = fromBeginning ? (uint)FIND_FROMSTART : FIND_FROMEND;
            return FindStringOrdinal(positionFlag, source, value, ignoreCase);
        }
 
        internal static int NlsLastIndexOfOrdinalCore(string source, string value, int startIndex, int count, bool ignoreCase)
        {
            Debug.Assert(!GlobalizationMode.Invariant);
            Debug.Assert(GlobalizationMode.UseNls);
 
            Debug.Assert(source != null);
            Debug.Assert(value != null);
 
            int offset = startIndex - count + 1;
            int result = FindStringOrdinal(FIND_FROMEND, source.AsSpan(offset, count), value, ignoreCase);
            if (result >= 0)
            {
                result += offset;
            }
            return result;
        }
 
        private unsafe int NlsGetHashCodeOfString(ReadOnlySpan<char> source, CompareOptions options)
        {
            Debug.Assert(!GlobalizationMode.Invariant);
            Debug.Assert(GlobalizationMode.UseNls);
            Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0);
 
#if TARGET_WINDOWS
            if (!Environment.IsWindows8OrAbove)
            {
                // On Windows 7 / Server 2008, LCMapStringEx exhibits strange behaviors if the destination
                // buffer is both non-null and too small for the required output. To prevent this from
                // causing issues for us, we need to make an immutable copy of the input buffer so that
                // its contents can't change between when we calculate the required sort key length and
                // when we populate the sort key buffer.
 
                source = source.ToString();
            }
#endif
 
            // LCMapStringEx doesn't support passing cchSrc = 0, so if given a null or empty input
            // we'll normalize it to an empty null-terminated string and pass -1 to indicate that
            // the underlying OS function should read until it encounters the null terminator.
 
            int sourceLength = source.Length;
            if (sourceLength == 0)
            {
                source = string.Empty;
                sourceLength = -1;
            }
 
            uint flags = LCMAP_SORTKEY | (uint)GetNativeCompareFlags(options);
 
            fixed (char* pSource = &MemoryMarshal.GetReference(source))
            {
                int sortKeyLength = Interop.Kernel32.LCMapStringEx(_sortHandle != IntPtr.Zero ? null : _sortName,
                                                  flags,
                                                  pSource, sourceLength /* in chars */,
                                                  null, 0,
                                                  null, null, _sortHandle);
                if (sortKeyLength == 0)
                {
                    throw new ArgumentException(SR.Arg_ExternalException);
                }
 
                // Note in calls to LCMapStringEx below, the input buffer is specified in wchars (and wchar count),
                // but the output buffer is specified in bytes (and byte count). This is because when generating
                // sort keys, LCMapStringEx treats the output buffer as containing opaque binary data.
                // See https://docs.microsoft.com/en-us/windows/desktop/api/winnls/nf-winnls-lcmapstringex.
 
                byte[]? borrowedArr = null;
                Span<byte> span = sortKeyLength <= 512 ?
                    stackalloc byte[512] :
                    (borrowedArr = ArrayPool<byte>.Shared.Rent(sortKeyLength));
 
                fixed (byte* pSortKey = &MemoryMarshal.GetReference(span))
                {
                    if (Interop.Kernel32.LCMapStringEx(_sortHandle != IntPtr.Zero ? null : _sortName,
                                                      flags,
                                                      pSource, sourceLength /* in chars */,
                                                      pSortKey, sortKeyLength,
                                                      null, null, _sortHandle) != sortKeyLength)
                    {
                        throw new ArgumentException(SR.Arg_ExternalException);
                    }
                }
 
                int hash = Marvin.ComputeHash32(span.Slice(0, sortKeyLength), Marvin.DefaultSeed);
 
                // Return the borrowed array if necessary.
                if (borrowedArr != null)
                {
                    ArrayPool<byte>.Shared.Return(borrowedArr);
                }
 
                return hash;
            }
        }
 
        internal static unsafe int NlsCompareStringOrdinalIgnoreCase(ref char string1, int count1, ref char string2, int count2)
        {
            Debug.Assert(!GlobalizationMode.Invariant);
            Debug.Assert(GlobalizationMode.UseNls);
 
            Debug.Assert(count1 > 0);
            Debug.Assert(count2 > 0);
 
            fixed (char* char1 = &string1)
            fixed (char* char2 = &string2)
            {
                Debug.Assert(char1 != null);
                Debug.Assert(char2 != null);
 
                // Use the OS to compare and then convert the result to expected value by subtracting 2
                int result = Interop.Kernel32.CompareStringOrdinal(char1, count1, char2, count2, bIgnoreCase: true);
                if (result == 0)
                {
                    throw new ArgumentException(SR.Arg_ExternalException);
                }
                return result - 2;
            }
        }
 
        private unsafe int NlsCompareString(ReadOnlySpan<char> string1, ReadOnlySpan<char> string2, CompareOptions options)
        {
            Debug.Assert(!GlobalizationMode.Invariant);
            Debug.Assert(GlobalizationMode.UseNls);
            Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0);
 
            string? localeName = _sortHandle != IntPtr.Zero ? null : _sortName;
 
            // CompareStringEx may try to dereference the first character of its input, even if an explicit
            // length of 0 is specified. To work around potential AVs we'll always ensure zero-length inputs
            // are normalized to a null-terminated empty string.
 
            if (string1.IsEmpty)
            {
                string1 = string.Empty;
            }
 
            if (string2.IsEmpty)
            {
                string2 = string.Empty;
            }
 
            fixed (char* pLocaleName = localeName)
            fixed (char* pString1 = &MemoryMarshal.GetReference(string1))
            fixed (char* pString2 = &MemoryMarshal.GetReference(string2))
            {
                Debug.Assert(*pString1 >= 0); // assert that we can always dereference this
                Debug.Assert(*pString2 >= 0); // assert that we can always dereference this
 
                int result = Interop.Kernel32.CompareStringEx(
                                    pLocaleName,
                                    (uint)GetNativeCompareFlags(options),
                                    pString1,
                                    string1.Length,
                                    pString2,
                                    string2.Length,
                                    null,
                                    null,
                                    _sortHandle);
 
                if (result == 0)
                {
                    throw new ArgumentException(SR.Arg_ExternalException);
                }
 
                // Map CompareStringEx return value to -1, 0, 1.
                return result - 2;
            }
        }
 
        private unsafe int FindString(
                    uint dwFindNLSStringFlags,
                    ReadOnlySpan<char> lpStringSource,
                    ReadOnlySpan<char> lpStringValue,
                    int* pcchFound)
        {
            Debug.Assert(!GlobalizationMode.Invariant);
            Debug.Assert(!lpStringValue.IsEmpty);
 
            string? localeName = _sortHandle != IntPtr.Zero ? null : _sortName;
 
            // FindNLSStringEx disallows passing an explicit 0 for cchSource or cchValue.
            // The caller should've already checked that 'lpStringValue' isn't empty,
            // but it's possible for 'lpStringSource' to be empty. In this case we'll
            // substitute an empty null-terminated string and pass -1 so that the NLS
            // function uses the implicit string length.
 
            int lpStringSourceLength = lpStringSource.Length;
            if (lpStringSourceLength == 0)
            {
                lpStringSource = string.Empty;
                lpStringSourceLength = -1;
            }
 
            fixed (char* pLocaleName = localeName)
            fixed (char* pSource = &MemoryMarshal.GetReference(lpStringSource))
            fixed (char* pValue = &MemoryMarshal.GetReference(lpStringValue))
            {
                Debug.Assert(pSource != null && pValue != null);
 
                int result = Interop.Kernel32.FindNLSStringEx(
                                    pLocaleName,
                                    dwFindNLSStringFlags,
                                    pSource,
                                    lpStringSourceLength,
                                    pValue,
                                    lpStringValue.Length,
                                    pcchFound,
                                    null,
                                    null,
                                    _sortHandle);
 
                Debug.Assert(result >= -1 && result <= lpStringSource.Length);
 
                // SetLastError is only performed under debug builds.
                Debug.Assert(result >= 0 || Marshal.GetLastPInvokeError() == Interop.Errors.ERROR_SUCCESS);
 
                return result;
            }
        }
 
        private unsafe int NlsIndexOfCore(ReadOnlySpan<char> source, ReadOnlySpan<char> target, CompareOptions options, int* matchLengthPtr, bool fromBeginning)
        {
            Debug.Assert(!GlobalizationMode.Invariant);
            Debug.Assert(GlobalizationMode.UseNls);
 
            Debug.Assert(target.Length != 0);
 
            uint positionFlag = fromBeginning ? (uint)FIND_FROMSTART : FIND_FROMEND;
            return FindString(positionFlag | (uint)GetNativeCompareFlags(options), source, target, matchLengthPtr);
        }
 
        private unsafe bool NlsStartsWith(ReadOnlySpan<char> source, ReadOnlySpan<char> prefix, CompareOptions options, int* matchLengthPtr)
        {
            Debug.Assert(!GlobalizationMode.Invariant);
            Debug.Assert(GlobalizationMode.UseNls);
 
            Debug.Assert(!prefix.IsEmpty);
            Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0);
 
            int idx = FindString(FIND_STARTSWITH | (uint)GetNativeCompareFlags(options), source, prefix, matchLengthPtr);
            if (idx >= 0)
            {
                if (matchLengthPtr != null)
                {
                    *matchLengthPtr += idx; // account for chars we skipped at the front of the string
                }
                return true;
            }
 
            return false;
        }
 
        private unsafe bool NlsEndsWith(ReadOnlySpan<char> source, ReadOnlySpan<char> suffix, CompareOptions options, int* matchLengthPtr)
        {
            Debug.Assert(!GlobalizationMode.Invariant);
            Debug.Assert(GlobalizationMode.UseNls);
 
            Debug.Assert(!suffix.IsEmpty);
            Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0);
 
            int idx = FindString(FIND_ENDSWITH | (uint)GetNativeCompareFlags(options), source, suffix, pcchFound: null);
            if (idx >= 0)
            {
                if (matchLengthPtr != null)
                {
                    *matchLengthPtr = source.Length - idx; // all chars from idx to the end of the string are consumed
                }
                return true;
            }
 
            return false;
        }
 
        private const uint LCMAP_SORTKEY = 0x00000400;
 
        private const int FIND_STARTSWITH = 0x00100000;
        private const int FIND_ENDSWITH = 0x00200000;
        private const int FIND_FROMSTART = 0x00400000;
        private const int FIND_FROMEND = 0x00800000;
 
        private unsafe SortKey NlsCreateSortKey(string source, CompareOptions options)
        {
            ArgumentNullException.ThrowIfNull(source);
 
            Debug.Assert(!GlobalizationMode.Invariant);
            Debug.Assert(GlobalizationMode.UseNls);
 
            if ((options & ValidCompareMaskOffFlags) != 0)
            {
                throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
            }
 
            byte[] keyData;
            uint flags = LCMAP_SORTKEY | (uint)GetNativeCompareFlags(options);
 
            // LCMapStringEx doesn't support passing cchSrc = 0, so if given an empty string
            // we'll instead pass -1 to indicate a null-terminated empty string.
 
            int sourceLength = source.Length;
            if (sourceLength == 0)
            {
                sourceLength = -1;
            }
 
            fixed (char* pSource = source)
            {
                int sortKeyLength = Interop.Kernel32.LCMapStringEx(_sortHandle != IntPtr.Zero ? null : _sortName,
                                            flags,
                                            pSource, sourceLength,
                                            null, 0,
                                            null, null, _sortHandle);
                if (sortKeyLength == 0)
                {
                    throw new ArgumentException(SR.Arg_ExternalException);
                }
 
                keyData = new byte[sortKeyLength];
 
                fixed (byte* pBytes = keyData)
                {
                    if (Interop.Kernel32.LCMapStringEx(_sortHandle != IntPtr.Zero ? null : _sortName,
                                            flags,
                                            pSource, sourceLength,
                                            pBytes, keyData.Length,
                                            null, null, _sortHandle) != sortKeyLength)
                    {
                        throw new ArgumentException(SR.Arg_ExternalException);
                    }
                }
            }
 
            return new SortKey(this, source, options, keyData);
        }
 
        private unsafe int NlsGetSortKey(ReadOnlySpan<char> source, Span<byte> destination, CompareOptions options)
        {
            Debug.Assert(!GlobalizationMode.Invariant);
            Debug.Assert((options & ValidCompareMaskOffFlags) == 0);
 
            // LCMapStringEx doesn't allow cchDest = 0 unless we're trying to query
            // the total number of bytes necessary.
 
            if (destination.IsEmpty)
            {
                ThrowHelper.ThrowArgumentException_DestinationTooShort();
            }
 
#if TARGET_WINDOWS
            if (!Environment.IsWindows8OrAbove)
            {
                // On Windows 7 / Server 2008, LCMapStringEx exhibits strange behaviors if the destination
                // buffer is both non-null and too small for the required output. To prevent this from
                // causing issues for us, we need to make an immutable copy of the input buffer so that
                // its contents can't change between when we calculate the required sort key length and
                // when we populate the sort key buffer.
 
                source = source.ToString();
            }
#endif
 
            uint flags = LCMAP_SORTKEY | (uint)GetNativeCompareFlags(options);
 
            // LCMapStringEx doesn't support passing cchSrc = 0, so if given an empty span
            // we'll instead normalize to a null-terminated empty string and pass -1 as
            // the length to indicate that the implicit null terminator should be used.
 
            int sourceLength = source.Length;
            if (sourceLength == 0)
            {
                source = string.Empty;
                sourceLength = -1;
            }
 
            int actualSortKeyLength;
 
            fixed (char* pSource = &MemoryMarshal.GetReference(source))
            fixed (byte* pSortKey = &MemoryMarshal.GetReference(destination))
            {
                Debug.Assert(pSource != null);
                Debug.Assert(pSortKey != null);
 
#if TARGET_WINDOWS
                if (!Environment.IsWindows8OrAbove)
                {
                    // Manually check that the destination buffer is large enough to hold the full output.
                    // See earlier comment for reasoning.
 
                    int requiredSortKeyLength = Interop.Kernel32.LCMapStringEx(_sortHandle != IntPtr.Zero ? null : _sortName,
                                                                               flags,
                                                                               pSource, sourceLength,
                                                                               null, 0,
                                                                               null, null, _sortHandle);
 
                    if (requiredSortKeyLength > destination.Length)
                    {
                        ThrowHelper.ThrowArgumentException_DestinationTooShort();
                    }
 
                    if (requiredSortKeyLength <= 0)
                    {
                        throw new ArgumentException(SR.Arg_ExternalException);
                    }
                }
#endif
 
                actualSortKeyLength = Interop.Kernel32.LCMapStringEx(_sortHandle != IntPtr.Zero ? null : _sortName,
                                                                     flags,
                                                                     pSource, sourceLength,
                                                                     pSortKey, destination.Length,
                                                                     null, null, _sortHandle);
            }
 
            if (actualSortKeyLength <= 0)
            {
                Debug.Assert(actualSortKeyLength == 0, "LCMapStringEx should never return a negative value.");
 
                // This could fail for a variety of reasons, including NLS being unable
                // to allocate a temporary buffer large enough to hold intermediate state,
                // or the destination buffer being too small.
 
                if (Marshal.GetLastPInvokeError() == Interop.Errors.ERROR_INSUFFICIENT_BUFFER)
                {
                    ThrowHelper.ThrowArgumentException_DestinationTooShort();
                }
                else
                {
                    throw new ArgumentException(SR.Arg_ExternalException);
                }
            }
 
            Debug.Assert(actualSortKeyLength <= destination.Length);
            return actualSortKeyLength;
        }
 
        private unsafe int NlsGetSortKeyLength(ReadOnlySpan<char> source, CompareOptions options)
        {
            Debug.Assert(!GlobalizationMode.Invariant);
            Debug.Assert((options & ValidCompareMaskOffFlags) == 0);
 
            uint flags = LCMAP_SORTKEY | (uint)GetNativeCompareFlags(options);
 
            // LCMapStringEx doesn't support passing cchSrc = 0, so if given an empty span
            // we'll instead normalize to a null-terminated empty string and pass -1 as
            // the length to indicate that the implicit null terminator should be used.
 
            int sourceLength = source.Length;
            if (sourceLength == 0)
            {
                source = string.Empty;
                sourceLength = -1;
            }
 
            int sortKeyLength;
 
            fixed (char* pSource = &MemoryMarshal.GetReference(source))
            {
                Debug.Assert(pSource != null);
                sortKeyLength = Interop.Kernel32.LCMapStringEx(_sortHandle != IntPtr.Zero ? null : _sortName,
                                                               flags,
                                                               pSource, sourceLength,
                                                               null, 0,
                                                               null, null, _sortHandle);
            }
 
            if (sortKeyLength <= 0)
            {
                Debug.Assert(sortKeyLength == 0, "LCMapStringEx should never return a negative value.");
 
                // This could fail for a variety of reasons, including NLS being unable
                // to allocate a temporary buffer large enough to hold intermediate state.
 
                throw new ArgumentException(SR.Arg_ExternalException);
            }
 
            return sortKeyLength;
        }
 
        private static unsafe bool NlsIsSortable(ReadOnlySpan<char> text)
        {
            Debug.Assert(!GlobalizationMode.Invariant);
            Debug.Assert(GlobalizationMode.UseNls);
            Debug.Assert(!text.IsEmpty);
 
            fixed (char* pText = &MemoryMarshal.GetReference(text))
            {
                return Interop.Kernel32.IsNLSDefinedString(Interop.Kernel32.COMPARE_STRING, 0, IntPtr.Zero, pText, text.Length);
            }
        }
 
        private const int COMPARE_OPTIONS_ORDINAL = 0x40000000;       // Ordinal
        private const int NORM_IGNORECASE = 0x00000001;       // Ignores case.  (use LINGUISTIC_IGNORECASE instead)
        private const int NORM_IGNOREKANATYPE = 0x00010000;       // Does not differentiate between Hiragana and Katakana characters. Corresponding Hiragana and Katakana will compare as equal.
        private const int NORM_IGNORENONSPACE = 0x00000002;       // Ignores nonspacing. This flag also removes Japanese accent characters.  (use LINGUISTIC_IGNOREDIACRITIC instead)
        private const int NORM_IGNORESYMBOLS = 0x00000004;       // Ignores symbols.
        private const int NORM_IGNOREWIDTH = 0x00020000;       // Does not differentiate between a single-byte character and the same character as a double-byte character.
        private const int NORM_LINGUISTIC_CASING = 0x08000000;       // use linguistic rules for casing
        private const int SORT_STRINGSORT = 0x00001000;       // Treats punctuation the same as symbols.
 
        private static int GetNativeCompareFlags(CompareOptions options)
        {
            // Use "linguistic casing" by default (load the culture's casing exception tables)
            int nativeCompareFlags = NORM_LINGUISTIC_CASING;
 
            if ((options & CompareOptions.IgnoreCase) != 0) { nativeCompareFlags |= NORM_IGNORECASE; }
            if ((options & CompareOptions.IgnoreKanaType) != 0) { nativeCompareFlags |= NORM_IGNOREKANATYPE; }
            if ((options & CompareOptions.IgnoreNonSpace) != 0) { nativeCompareFlags |= NORM_IGNORENONSPACE; }
            if ((options & CompareOptions.IgnoreSymbols) != 0) { nativeCompareFlags |= NORM_IGNORESYMBOLS; }
            if ((options & CompareOptions.IgnoreWidth) != 0) { nativeCompareFlags |= NORM_IGNOREWIDTH; }
            if ((options & CompareOptions.StringSort) != 0) { nativeCompareFlags |= SORT_STRINGSORT; }
 
            // TODO: Can we try for GetNativeCompareFlags to never
            // take Ordinal or OrdinalIgnoreCase.  This value is not part of Win32, we just handle it special
            // in some places.
            // Suffix & Prefix shouldn't use this, make sure to turn off the NORM_LINGUISTIC_CASING flag
            if (options == CompareOptions.Ordinal) { nativeCompareFlags = COMPARE_OPTIONS_ORDINAL; }
 
            Debug.Assert(((options & ~(CompareOptions.IgnoreCase |
                                          CompareOptions.IgnoreKanaType |
                                          CompareOptions.IgnoreNonSpace |
                                          CompareOptions.IgnoreSymbols |
                                          CompareOptions.IgnoreWidth |
                                          CompareOptions.StringSort)) == 0) ||
                             (options == CompareOptions.Ordinal), "[CompareInfo.GetNativeCompareFlags]Expected all flags to be handled");
 
            return nativeCompareFlags;
        }
 
        private unsafe SortVersion NlsGetSortVersion()
        {
            Debug.Assert(!GlobalizationMode.Invariant);
            Debug.Assert(GlobalizationMode.UseNls);
 
            Interop.Kernel32.NlsVersionInfoEx nlsVersion = default;
            nlsVersion.dwNLSVersionInfoSize = sizeof(Interop.Kernel32.NlsVersionInfoEx);
            Interop.Kernel32.GetNLSVersionEx(Interop.Kernel32.COMPARE_STRING, _sortName, &nlsVersion);
            return new SortVersion(
                        nlsVersion.dwNLSVersion,
                        nlsVersion.dwEffectiveId == 0 ? LCID : nlsVersion.dwEffectiveId,
                        nlsVersion.guidCustomVersion);
        }
    }
}