File: src\libraries\System.Private.CoreLib\src\System\Globalization\CultureData.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.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
 
#pragma warning disable 8500 // taking address of managed type
 
namespace System.Globalization
{
    internal sealed partial class CultureData
    {
        // Wrappers around the GetLocaleInfoEx APIs which handle marshalling the returned
        // data as either and Int or string.
        internal static unsafe string? GetLocaleInfoEx(string localeName, uint field)
        {
            // REVIEW: Determine the maximum size for the buffer
            const int BUFFER_SIZE = 530;
 
            char* pBuffer = stackalloc char[BUFFER_SIZE];
            int resultCode = GetLocaleInfoEx(localeName, field, pBuffer, BUFFER_SIZE);
            if (resultCode > 0)
            {
                return new string(pBuffer);
            }
 
            return null;
        }
 
        internal static unsafe int GetLocaleInfoExInt(string localeName, uint field)
        {
            field |= Interop.Kernel32.LOCALE_RETURN_NUMBER;
            int value = 0;
            GetLocaleInfoEx(localeName, field, (char*)&value, sizeof(int));
            return value;
        }
 
        internal static unsafe int GetLocaleInfoEx(string lpLocaleName, uint lcType, char* lpLCData, int cchData)
        {
            Debug.Assert(!GlobalizationMode.Invariant);
 
            return Interop.Kernel32.GetLocaleInfoEx(lpLocaleName, lcType, lpLCData, cchData);
        }
 
        private string NlsGetLocaleInfo(LocaleStringData type)
        {
            Debug.Assert(ShouldUseUserOverrideNlsData);
            Debug.Assert(_sRealName != null, "[CultureData.DoGetLocaleInfo] Expected _sRealName to be populated by already");
            return NlsGetLocaleInfo(_sRealName, type);
        }
 
        // For LOCALE_SPARENT we need the option of using the "real" name (forcing neutral names) instead of the
        // "windows" name, which can be specific for downlevel (< windows 7) os's.
        private string NlsGetLocaleInfo(string localeName, LocaleStringData type)
        {
            Debug.Assert(ShouldUseUserOverrideNlsData);
            uint lctype = (uint)type;
 
            return GetLocaleInfoFromLCType(localeName, lctype, _bUseOverrides);
        }
 
        private int NlsGetLocaleInfo(LocaleNumberData type)
        {
            Debug.Assert(IsWin32Installed);
            uint lctype = (uint)type;
 
            // Fix lctype if we don't want overrides
            if (!_bUseOverrides)
            {
                lctype |= Interop.Kernel32.LOCALE_NOUSEROVERRIDE;
            }
 
            // Ask OS for data, note that we presume it returns success, so we have to know that
            // sWindowsName is valid before calling.
            Debug.Assert(_sRealName != null, "[CultureData.DoGetLocaleInfoInt] Expected _sRealName to be populated by already");
            return GetLocaleInfoExInt(_sRealName, lctype);
        }
 
        private int[] NlsGetLocaleInfo(LocaleGroupingData type)
        {
            Debug.Assert(ShouldUseUserOverrideNlsData);
            Debug.Assert(_sRealName != null, "[CultureData.DoGetLocaleInfoInt] Expected _sRealName to be populated by already");
            return ConvertWin32GroupString(GetLocaleInfoFromLCType(_sRealName, (uint)type, _bUseOverrides));
        }
 
        internal static bool NlsIsEnsurePredefinedLocaleName(string name)
        {
            Debug.Assert(GlobalizationMode.UseNls);
            return GetLocaleInfoExInt(name, Interop.Kernel32.LOCALE_ICONSTRUCTEDLOCALE) != 1;
        }
 
        private string? NlsGetTimeFormatString()
        {
            Debug.Assert(ShouldUseUserOverrideNlsData);
            Debug.Assert(_sRealName != null, "[CultureData.DoGetLocaleInfoInt] Expected _sRealName to be populated by already");
            return ReescapeWin32String(GetLocaleInfoFromLCType(_sRealName, Interop.Kernel32.LOCALE_STIMEFORMAT, _bUseOverrides));
        }
 
        private int NlsGetFirstDayOfWeek()
        {
            Debug.Assert(ShouldUseUserOverrideNlsData);
            Debug.Assert(_sRealName != null, "[CultureData.DoGetLocaleInfoInt] Expected _sRealName to be populated by already");
 
            int result = GetLocaleInfoExInt(_sRealName, Interop.Kernel32.LOCALE_IFIRSTDAYOFWEEK | (!_bUseOverrides ? Interop.Kernel32.LOCALE_NOUSEROVERRIDE : 0));
 
            // Win32 and .NET disagree on the numbering for days of the week, so we have to convert.
            return ConvertFirstDayOfWeekMonToSun(result);
        }
 
        // Enumerate all system cultures and then try to find out which culture has
        // region name match the requested region name
        private static CultureData? NlsGetCultureDataFromRegionName(string regionName)
        {
            Debug.Assert(!GlobalizationMode.Invariant);
            Debug.Assert(GlobalizationMode.UseNls);
            Debug.Assert(regionName != null);
 
            EnumLocaleData context;
            context.cultureName = null;
            context.regionName = regionName;
 
            unsafe
            {
                Interop.Kernel32.EnumSystemLocalesEx(&EnumSystemLocalesProc, Interop.Kernel32.LOCALE_SPECIFICDATA | Interop.Kernel32.LOCALE_SUPPLEMENTAL, &context, IntPtr.Zero);
            }
 
            if (context.cultureName != null)
            {
                // we got a matched culture
                return GetCultureData(context.cultureName, true);
            }
 
            return null;
        }
 
        private string NlsGetLanguageDisplayName(string cultureName)
        {
            Debug.Assert(GlobalizationMode.UseNls);
 
            // Usually the UI culture shouldn't be different than what we got from WinRT except
            // if DefaultThreadCurrentUICulture was set
            CultureInfo? ci;
 
            if (CultureInfo.DefaultThreadCurrentUICulture != null &&
                ((ci = CultureInfo.GetUserDefaultCulture()) != null) &&
                !CultureInfo.DefaultThreadCurrentUICulture.Name.Equals(ci.Name))
            {
                return NativeName;
            }
            else
            {
                return NlsGetLocaleInfo(cultureName, LocaleStringData.LocalizedDisplayName);
            }
        }
 
        private string NlsGetRegionDisplayName()
        {
            Debug.Assert(GlobalizationMode.UseNls);
 
            // If the current UI culture matching the OS UI language, we'll get the display name from the OS.
            // otherwise, we use the native name as we don't carry resources for the region display names anyway.
            if (CultureInfo.CurrentUICulture.Name.Equals(CultureInfo.UserDefaultUICulture.Name))
            {
                return NlsGetLocaleInfo(LocaleStringData.LocalizedCountryName);
            }
 
            return NativeCountryName;
        }
 
        // PAL methods end here.
 
        private static string GetLocaleInfoFromLCType(string localeName, uint lctype, bool useUserOverride)
        {
            Debug.Assert(localeName != null, "[CultureData.GetLocaleInfoFromLCType] Expected localeName to be not be null");
 
            // Fix lctype if we don't want overrides
            if (!useUserOverride)
            {
                lctype |= Interop.Kernel32.LOCALE_NOUSEROVERRIDE;
            }
 
            // Ask OS for data
            // Failed? Just use empty string
            return GetLocaleInfoEx(localeName, lctype) ?? string.Empty;
        }
 
        /// <summary>
        /// Reescape a Win32 style quote string as a NLS+ style quoted string
        ///
        /// This is also the escaping style used by custom culture data files
        ///
        /// NLS+ uses \ to escape the next character, whether in a quoted string or
        /// not, so we always have to change \ to \\.
        ///
        /// NLS+ uses \' to escape a quote inside a quoted string so we have to change
        /// '' to \' (if inside a quoted string)
        ///
        /// We don't build the stringbuilder unless we find something to change
        /// </summary>
        [return: NotNullIfNotNull(nameof(str))]
        internal static string? ReescapeWin32String(string? str)
        {
            // If we don't have data, then don't try anything
            if (str == null)
            {
                return null;
            }
 
            StringBuilder? result = null;
 
            bool inQuote = false;
            for (int i = 0; i < str.Length; i++)
            {
                // Look for quote
                if (str[i] == '\'')
                {
                    // Already in quote?
                    if (inQuote)
                    {
                        // See another single quote.  Is this '' of 'fred''s' or '''', or is it an ending quote?
                        if (i + 1 < str.Length && str[i + 1] == '\'')
                        {
                            // Found another ', so we have ''.  Need to add \' instead.
                            // 1st make sure we have our stringbuilder
                            result ??= new StringBuilder(str, 0, i, str.Length * 2);
 
                            // Append a \' and keep going (so we don't turn off quote mode)
                            result.Append("\\'");
                            i++;
                            continue;
                        }
 
                        // Turning off quote mode, fall through to add it
                        inQuote = false;
                    }
                    else
                    {
                        // Found beginning quote, fall through to add it
                        inQuote = true;
                    }
                }
                // Is there a single \ character?
                else if (str[i] == '\\')
                {
                    // Found a \, need to change it to \\
                    // 1st make sure we have our stringbuilder
                    result ??= new StringBuilder(str, 0, i, str.Length * 2);
 
                    // Append our \\ to the string & continue
                    result.Append("\\\\");
                    continue;
                }
 
                // If we have a builder we need to add our character
                result?.Append(str[i]);
            }
 
            // Unchanged string? , just return input string
            if (result == null)
                return str;
 
            // String changed, need to use the builder
            return result.ToString();
        }
 
        [return: NotNullIfNotNull(nameof(array))]
        internal static string[]? ReescapeWin32Strings(string[]? array)
        {
            if (array != null)
            {
                for (int i = 0; i < array.Length; i++)
                {
                    array[i] = ReescapeWin32String(array[i]);
                }
            }
 
            return array;
        }
 
        // If we get a group from windows, then its in 3;0 format with the 0 backwards
        // of how NLS+ uses it (ie: if the string has a 0, then the int[] shouldn't and vice versa)
        // EXCEPT in the case where the list only contains 0 in which NLS and NLS+ have the same meaning.
        private static int[] ConvertWin32GroupString(string win32Str)
        {
            // None of these cases make any sense
            if (string.IsNullOrEmpty(win32Str))
            {
                return new int[] { 3 };
            }
 
            if (win32Str.StartsWith('0'))
            {
                return new int[] { 0 };
            }
 
            // Since its in n;n;n;n;n format, we can always get the length quickly
            int[] values;
            if (win32Str.EndsWith('0'))
            {
                // Trailing 0 gets dropped. 1;0 -> 1
                values = new int[win32Str.Length / 2];
            }
            else
            {
                // Need extra space for trailing zero 1 -> 1;0
                values = new int[(win32Str.Length / 2) + 2];
                values[^1] = 0;
            }
 
            int i;
            int j;
            for (i = 0, j = 0; i < win32Str.Length && j < values.Length; i += 2, j++)
            {
                // Note that this # shouldn't ever be zero, 'cause 0 is only at end
                // But we'll test because its registry that could be anything
                if (!char.IsBetween(win32Str[i], '1', '9'))
                    return new int[] { 3 };
 
                values[j] = (int)(win32Str[i] - '0');
            }
 
            return values;
        }
 
        private static int ConvertFirstDayOfWeekMonToSun(int iTemp)
        {
            // Convert Mon-Sun to Sun-Sat format
            iTemp++;
            if (iTemp > 6)
            {
                // Wrap Sunday and convert invalid data to Sunday
                iTemp = 0;
            }
            return iTemp;
        }
 
        // Context for EnumCalendarInfoExEx callback.
        private struct EnumLocaleData
        {
            public string regionName;
            public string? cultureName;
        }
 
        // EnumSystemLocaleEx callback.
        [UnmanagedCallersOnly]
        private static unsafe Interop.BOOL EnumSystemLocalesProc(char* lpLocaleString, uint flags, void* contextHandle)
        {
            EnumLocaleData* context = (EnumLocaleData*)contextHandle;
            try
            {
                string cultureName = new string(lpLocaleString);
                string? regionName = GetLocaleInfoEx(cultureName, Interop.Kernel32.LOCALE_SISO3166CTRYNAME);
                if (regionName != null && regionName.Equals(context->regionName, StringComparison.OrdinalIgnoreCase))
                {
                    context->cultureName = cultureName;
                    return Interop.BOOL.FALSE; // we found a match, then stop the enumeration
                }
 
                return Interop.BOOL.TRUE;
            }
            catch (Exception)
            {
                return Interop.BOOL.FALSE;
            }
        }
 
        // EnumSystemLocaleEx callback.
        [UnmanagedCallersOnly]
        private static unsafe Interop.BOOL EnumAllSystemLocalesProc(char* lpLocaleString, uint flags, void* contextHandle)
        {
            try
            {
                ((EnumData*)contextHandle)->strings.Add(new string(lpLocaleString));
                return Interop.BOOL.TRUE;
            }
            catch (Exception)
            {
                return Interop.BOOL.FALSE;
            }
        }
 
        // Context for EnumTimeFormatsEx callback.
        private struct EnumData
        {
            public List<string> strings;
        }
 
        // EnumTimeFormatsEx callback itself.
        [UnmanagedCallersOnly]
        private static unsafe Interop.BOOL EnumTimeCallback(char* lpTimeFormatString, void* lParam)
        {
            try
            {
                ((EnumData*)lParam)->strings.Add(new string(lpTimeFormatString));
                return Interop.BOOL.TRUE;
            }
            catch (Exception)
            {
                return Interop.BOOL.FALSE;
            }
        }
 
        private static unsafe string[]? nativeEnumTimeFormats(string localeName, uint dwFlags, bool useUserOverride)
        {
            EnumData data = default;
            data.strings = new List<string>();
 
            // Now call the enumeration API. Work is done by our callback function
            Interop.Kernel32.EnumTimeFormatsEx(&EnumTimeCallback, localeName, dwFlags, &data);
 
            if (data.strings.Count > 0)
            {
                // Now we need to allocate our stringarray and populate it
                string[] results = data.strings.ToArray();
 
                if (!useUserOverride && data.strings.Count > 1)
                {
                    // Since there is no "NoUserOverride" aware EnumTimeFormatsEx, we always get an override
                    // The override is the first entry if it is overridden.
                    // We can check if we have overrides by checking the GetLocaleInfo with no override
                    // If we do have an override, we don't know if it is a user defined override or if the
                    // user has just selected one of the predefined formats so we can't just remove it
                    // but we can move it down.
                    uint lcType = (dwFlags == Interop.Kernel32.TIME_NOSECONDS) ? Interop.Kernel32.LOCALE_SSHORTTIME : Interop.Kernel32.LOCALE_STIMEFORMAT;
                    string timeFormatNoUserOverride = GetLocaleInfoFromLCType(localeName, lcType, useUserOverride);
                    if (timeFormatNoUserOverride != "")
                    {
                        string firstTimeFormat = results[0];
                        if (timeFormatNoUserOverride != firstTimeFormat)
                        {
                            results[0] = results[1];
                            results[1] = firstTimeFormat;
                        }
                    }
                }
 
                return results;
            }
 
            return null;
        }
 
        private static int NlsLocaleNameToLCID(string cultureName)
        {
            Debug.Assert(!GlobalizationMode.Invariant);
 
            return Interop.Kernel32.LocaleNameToLCID(cultureName, Interop.Kernel32.LOCALE_ALLOW_NEUTRAL_NAMES);
        }
 
        private string NlsGetThreeLetterWindowsLanguageName(string cultureName)
        {
            Debug.Assert(GlobalizationMode.UseNls);
            return NlsGetLocaleInfo(cultureName, LocaleStringData.AbbreviatedWindowsLanguageName);
        }
 
        private static CultureInfo[] NlsEnumCultures(CultureTypes types)
        {
            Debug.Assert(!GlobalizationMode.Invariant);
            Debug.Assert(GlobalizationMode.UseNls);
 
            uint flags = 0;
 
#pragma warning disable 618
            if ((types & (CultureTypes.FrameworkCultures | CultureTypes.InstalledWin32Cultures | CultureTypes.ReplacementCultures)) != 0)
            {
                flags |= Interop.Kernel32.LOCALE_NEUTRALDATA | Interop.Kernel32.LOCALE_SPECIFICDATA;
            }
#pragma warning restore 618
 
            if ((types & CultureTypes.NeutralCultures) != 0)
            {
                flags |= Interop.Kernel32.LOCALE_NEUTRALDATA;
            }
 
            if ((types & CultureTypes.SpecificCultures) != 0)
            {
                flags |= Interop.Kernel32.LOCALE_SPECIFICDATA;
            }
 
            if ((types & CultureTypes.UserCustomCulture) != 0)
            {
                flags |= Interop.Kernel32.LOCALE_SUPPLEMENTAL;
            }
 
            if ((types & CultureTypes.ReplacementCultures) != 0)
            {
                flags |= Interop.Kernel32.LOCALE_SUPPLEMENTAL;
            }
 
            EnumData context = default;
            context.strings = new List<string>();
 
            unsafe
            {
                Interop.Kernel32.EnumSystemLocalesEx(&EnumAllSystemLocalesProc, flags, &context, IntPtr.Zero);
            }
 
            CultureInfo[] cultures = new CultureInfo[context.strings.Count];
            for (int i = 0; i < cultures.Length; i++)
            {
                cultures[i] = new CultureInfo(context.strings[i]);
            }
 
            return cultures;
        }
 
        private string NlsGetConsoleFallbackName(string cultureName)
        {
            Debug.Assert(GlobalizationMode.UseNls);
            return NlsGetLocaleInfo(cultureName, LocaleStringData.ConsoleFallbackName);
        }
 
        internal bool NlsIsReplacementCulture
        {
            get
            {
                Debug.Assert(GlobalizationMode.UseNls);
                EnumData context = default;
                context.strings = new List<string>();
 
                unsafe
                {
                    Interop.Kernel32.EnumSystemLocalesEx(&EnumAllSystemLocalesProc, Interop.Kernel32.LOCALE_REPLACEMENT, &context, IntPtr.Zero);
                }
 
                for (int i = 0; i < context.strings.Count; i++)
                {
                    if (string.Equals(context.strings[i], _sRealName, StringComparison.OrdinalIgnoreCase))
                        return true;
                }
 
                return false;
            }
        }
    }
}