From 04dac7b0fede29d44f896c5fd793754f83974175 Mon Sep 17 00:00:00 2001 From: Tarek Mahmoud Sayed Date: Thu, 1 Jul 2021 11:55:05 -0700 Subject: [PATCH] Allow restricting cultures creation with any arbitrary names in Globalization Invariant Mode (#54247) Co-authored-by: Eric Erhardt --- docs/workflow/trimming/feature-switches.md | 1 + .../tests/CultureInfo/GetCultureInfo.cs | 76 +++++++++++++++++++ .../tests/Invariant/InvariantMode.cs | 66 ++++++++++------ .../Invariant/runtimeconfig.template.json | 3 +- .../ILLink/ILLink.Substitutions.Shared.xml | 1 + .../src/Resources/Strings.resx | 3 + .../src/System/AppContextConfigHelper.cs | 7 +- .../src/System/Globalization/CultureData.cs | 5 +- .../src/System/Globalization/CultureInfo.cs | 10 ++- .../System/Globalization/GlobalizationMode.cs | 4 +- .../ManifestBasedResourceGroveler.cs | 2 +- .../src/System/TimeZoneInfo.Win32.cs | 6 +- .../InvariantGlobalizationTrue.cs | 25 +++--- .../System.Runtime.TrimmingTests.proj | 2 +- .../InvariantGlobalizationTests.cs | 12 ++- 15 files changed, 167 insertions(+), 56 deletions(-) diff --git a/docs/workflow/trimming/feature-switches.md b/docs/workflow/trimming/feature-switches.md index 9836a8375dba2..ac2ca01a2436e 100644 --- a/docs/workflow/trimming/feature-switches.md +++ b/docs/workflow/trimming/feature-switches.md @@ -13,6 +13,7 @@ configurations but their defaults might vary as any SDK can set the defaults dif | EnableUnsafeBinaryFormatterSerialization | System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization | BinaryFormatter serialization support is trimmed when set to false | | EventSourceSupport | System.Diagnostics.Tracing.EventSource.IsSupported | Any EventSource related code or logic is trimmed when set to false | | InvariantGlobalization | System.Globalization.Invariant | All globalization specific code and data is trimmed when set to true | +| PredefinedCulturesOnly | System.Globalization.PredefinedCulturesOnly | Don't allow creating a culture for which the platform does not have data | | UseSystemResourceKeys | System.Resources.UseSystemResourceKeys | Any localizable resources for system assemblies is trimmed when set to true | | HttpActivityPropagationSupport | System.Net.Http.EnableActivityPropagation | Any dependency related to diagnostics support for System.Net.Http is trimmed when set to false | | UseNativeHttpHandler | System.Net.Http.UseNativeHttpHandler | HttpClient uses by default platform native implementation of HttpMessageHandler if set to true. | diff --git a/src/libraries/System.Globalization/tests/CultureInfo/GetCultureInfo.cs b/src/libraries/System.Globalization/tests/CultureInfo/GetCultureInfo.cs index 0b4bdd5708992..e6399bb5cc617 100644 --- a/src/libraries/System.Globalization/tests/CultureInfo/GetCultureInfo.cs +++ b/src/libraries/System.Globalization/tests/CultureInfo/GetCultureInfo.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using Microsoft.DotNet.RemoteExecutor; using System.Diagnostics; +using System.Collections.Concurrent; using Xunit; namespace System.Globalization.Tests @@ -139,5 +140,80 @@ public void PredefinedCulturesOnlyEnvVarTest(string predefinedCulturesOnlyEnvVar } }, cultureName, predefinedCulturesOnlyEnvVar, new RemoteInvokeOptions { StartInfo = psi }).Dispose(); } + + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [InlineData(true, true, false)] + [InlineData(true, true, true)] + [InlineData(true, false, true)] + [InlineData(false, true, true)] + [InlineData(false, true, false)] + [InlineData(false, false, true)] + public void TestAllowInvariantCultureOnly(bool enableInvariant, bool predefinedCulturesOnly, bool declarePredefinedCulturesOnly) + { + var psi = new ProcessStartInfo(); + psi.Environment.Clear(); + + if (enableInvariant) + { + psi.Environment.Add("DOTNET_SYSTEM_GLOBALIZATION_INVARIANT", "true"); + } + + if (declarePredefinedCulturesOnly) + { + psi.Environment.Add("DOTNET_SYSTEM_GLOBALIZATION_PREDEFINED_CULTURES_ONLY", predefinedCulturesOnly ? "true" : "false"); + } + + bool restricted = enableInvariant && (declarePredefinedCulturesOnly ? predefinedCulturesOnly : true); + + RemoteExecutor.Invoke((invariantEnabled, isRestricted) => + { + bool restrictedMode = bool.Parse(isRestricted); + + // First ensure we can create the current cultures regardless of the mode we are in + Assert.NotNull(CultureInfo.CurrentCulture); + Assert.NotNull(CultureInfo.CurrentUICulture); + + // Invariant culture should be valid all the time + Assert.Equal("", new CultureInfo("").Name); + Assert.Equal("", CultureInfo.InvariantCulture.Name); + + if (restrictedMode) + { + Assert.Equal("", CultureInfo.CurrentCulture.Name); + Assert.Equal("", CultureInfo.CurrentUICulture.Name); + + // Throwing exception is testing accessing the resources in this restricted mode. + // We should retrieve the resources from the neutral resources in the main assemblies. + AssertExtensions.Throws(() => new CultureInfo("en-US")); + AssertExtensions.Throws(() => new CultureInfo("en")); + + AssertExtensions.Throws(() => new CultureInfo("ja-JP")); + AssertExtensions.Throws(() => new CultureInfo("es")); + + // Test throwing exceptions from non-core assemblies. + Exception exception = Record.Exception(() => new ConcurrentBag(null)); + Assert.NotNull(exception); + Assert.IsType(exception); + Assert.Equal("collection", (exception as ArgumentNullException).ParamName); + Assert.Equal("The collection argument is null. (Parameter 'collection')", exception.Message); + } + else + { + Assert.Equal("en-US", new CultureInfo("en-US").Name); + Assert.Equal("ja-JP", new CultureInfo("ja-JP").Name); + Assert.Equal("en", new CultureInfo("en").Name); + Assert.Equal("es", new CultureInfo("es").Name); + } + + // Ensure the Invariant Mode functionality still work + if (bool.Parse(invariantEnabled)) + { + Assert.True(CultureInfo.CurrentCulture.Calendar is GregorianCalendar); + Assert.True("abcd".Equals("ABCD", StringComparison.CurrentCultureIgnoreCase)); + Assert.Equal("Invariant Language (Invariant Country)", CultureInfo.CurrentCulture.NativeName); + } + + }, enableInvariant.ToString(), restricted.ToString(), new RemoteInvokeOptions { StartInfo = psi }).Dispose(); + } } } diff --git a/src/libraries/System.Globalization/tests/Invariant/InvariantMode.cs b/src/libraries/System.Globalization/tests/Invariant/InvariantMode.cs index 94ec2003e3157..a2cd0baf70f8e 100644 --- a/src/libraries/System.Globalization/tests/Invariant/InvariantMode.cs +++ b/src/libraries/System.Globalization/tests/Invariant/InvariantMode.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; +using System.Reflection; using System.Buffers.Binary; using System.Collections.Generic; using System.IO; @@ -13,6 +14,23 @@ namespace System.Globalization.Tests { public class InvariantModeTests { + private static bool PredefinedCulturesOnlyIsDisabled { get; } = !PredefinedCulturesOnly(); + private static bool PredefinedCulturesOnly() + { + bool ret; + + try + { + ret = (bool) typeof(object).Assembly.GetType("System.Globalization.GlobalizationMode").GetProperty("PredefinedCulturesOnly", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null); + } + catch + { + ret = false; + } + + return ret; + } + public static IEnumerable Cultures_TestData() { yield return new object[] { "en-US" }; @@ -490,13 +508,13 @@ public static IEnumerable GetUnicode_TestData() yield return new object[] { "xn--de-jg4avhby1noc0d", 0, 21, "\u30D1\u30D5\u30A3\u30FC\u0064\u0065\u30EB\u30F3\u30D0" }; } - [Fact] + [ConditionalFact(nameof(PredefinedCulturesOnlyIsDisabled))] public static void IcuShouldNotBeLoaded() { Assert.False(PlatformDetection.IsIcuGlobalization); } - [Theory] + [ConditionalTheory(nameof(PredefinedCulturesOnlyIsDisabled))] [MemberData(nameof(Cultures_TestData))] public void TestCultureData(string cultureName) { @@ -636,7 +654,7 @@ public void TestCultureData(string cultureName) Assert.True(cultureName.Equals(ci.CompareInfo.Name, StringComparison.OrdinalIgnoreCase)); } - [Theory] + [ConditionalTheory(nameof(PredefinedCulturesOnlyIsDisabled))] [MemberData(nameof(Cultures_TestData))] public void SetCultureData(string cultureName) { @@ -652,13 +670,13 @@ public void SetCultureData(string cultureName) Assert.Throws(() => ci.DateTimeFormat.Calendar = new TaiwanCalendar()); } - [Fact] + [ConditionalFact(nameof(PredefinedCulturesOnlyIsDisabled))] public void TestEnum() { Assert.Equal(new CultureInfo[1] { CultureInfo.InvariantCulture }, CultureInfo.GetCultures(CultureTypes.AllCultures)); } - [Theory] + [ConditionalTheory(nameof(PredefinedCulturesOnlyIsDisabled))] [MemberData(nameof(Cultures_TestData))] public void TestSortVersion(string cultureName) { @@ -670,7 +688,7 @@ public void TestSortVersion(string cultureName) Assert.Equal(version, new CultureInfo(cultureName).CompareInfo.Version); } - [Theory] + [ConditionalTheory(nameof(PredefinedCulturesOnlyIsDisabled))] [InlineData(0, 0)] [InlineData(1, 2)] [InlineData(100_000, 200_000)] @@ -683,7 +701,7 @@ public void TestGetSortKeyLength_Valid(int inputLength, int expectedSortKeyLengt Assert.Equal(expectedSortKeyLength, CultureInfo.InvariantCulture.CompareInfo.GetSortKeyLength(dummySpan)); } - [Theory] + [ConditionalTheory(nameof(PredefinedCulturesOnlyIsDisabled))] [InlineData(0x4000_0000)] [InlineData(int.MaxValue)] public unsafe void TestGetSortKeyLength_OverlongArgument(int inputLength) @@ -698,7 +716,7 @@ public unsafe void TestGetSortKeyLength_OverlongArgument(int inputLength) }); } - [Theory] + [ConditionalTheory(nameof(PredefinedCulturesOnlyIsDisabled))] [InlineData("Hello", CompareOptions.None, "Hello")] [InlineData("Hello", CompareOptions.IgnoreWidth, "Hello")] [InlineData("Hello", CompareOptions.IgnoreCase, "HELLO")] @@ -741,7 +759,7 @@ public unsafe void TestSortKey_FromSpan(string input, CompareOptions options, st } } - [Fact] + [ConditionalFact(nameof(PredefinedCulturesOnlyIsDisabled))] public void TestSortKey_ZeroWeightCodePoints() { // In the invariant globalization mode, there's no such thing as a zero-weight code point, @@ -753,7 +771,7 @@ public void TestSortKey_ZeroWeightCodePoints() Assert.NotEqual(0, SortKey.Compare(sortKeyForEmptyString, sortKeyForZeroWidthJoiner)); } - [Theory] + [ConditionalTheory(nameof(PredefinedCulturesOnlyIsDisabled))] [InlineData("", "", 0)] [InlineData("", "not-empty", -1)] [InlineData("not-empty", "", 1)] @@ -794,7 +812,7 @@ private static StringComparison GetStringComparison(CompareOptions options) return sc; } - [Theory] + [ConditionalTheory(nameof(PredefinedCulturesOnlyIsDisabled))] [MemberData(nameof(IndexOf_TestData))] public void TestIndexOf(string source, string value, int startIndex, int count, CompareOptions options, int result) { @@ -841,7 +859,7 @@ static void TestCore(CompareInfo compareInfo, string source, string value, int s } } - [Theory] + [ConditionalTheory(nameof(PredefinedCulturesOnlyIsDisabled))] [MemberData(nameof(LastIndexOf_TestData))] public void TestLastIndexOf(string source, string value, int startIndex, int count, CompareOptions options, int result) { @@ -901,7 +919,7 @@ static void TestCore(CompareInfo compareInfo, string source, string value, int s } } - [Theory] + [ConditionalTheory(nameof(PredefinedCulturesOnlyIsDisabled))] [MemberData(nameof(IsPrefix_TestData))] public void TestIsPrefix(string source, string value, CompareOptions options, bool result) { @@ -936,7 +954,7 @@ public void TestIsPrefix(string source, string value, CompareOptions options, bo } } - [Theory] + [ConditionalTheory(nameof(PredefinedCulturesOnlyIsDisabled))] [MemberData(nameof(IsSuffix_TestData))] public void TestIsSuffix(string source, string value, CompareOptions options, bool result) { @@ -971,7 +989,7 @@ public void TestIsSuffix(string source, string value, CompareOptions options, bo } } - [Theory] + [ConditionalTheory(nameof(PredefinedCulturesOnlyIsDisabled))] [InlineData("", false)] [InlineData('x', true)] [InlineData('\ud800', true)] // standalone high surrogate @@ -988,7 +1006,7 @@ public void TestIsSortable(object sourceObj, bool expectedResult) } } - [Theory] + [ConditionalTheory(nameof(PredefinedCulturesOnlyIsDisabled))] [MemberData(nameof(Compare_TestData))] public void TestCompare(string source, string value, CompareOptions options, int result) { @@ -1019,7 +1037,7 @@ public void TestCompare(string source, string value, CompareOptions options, int } - [Theory] + [ConditionalTheory(nameof(PredefinedCulturesOnlyIsDisabled))] [MemberData(nameof(ToLower_TestData))] public void TestToLower(string upper, string lower, bool result) { @@ -1030,7 +1048,7 @@ public void TestToLower(string upper, string lower, bool result) } } - [Theory] + [ConditionalTheory(nameof(PredefinedCulturesOnlyIsDisabled))] [MemberData(nameof(ToUpper_TestData))] public void TestToUpper(string lower, string upper, bool result) { @@ -1041,7 +1059,7 @@ public void TestToUpper(string lower, string upper, bool result) } } - [Theory] + [ConditionalTheory(nameof(PredefinedCulturesOnlyIsDisabled))] [InlineData("", NormalizationForm.FormC)] [InlineData("\uFB01", NormalizationForm.FormC)] [InlineData("\uFB01", NormalizationForm.FormD)] @@ -1063,7 +1081,7 @@ public void TestNormalization(string s, NormalizationForm form) Assert.Equal(s, s.Normalize(form)); } - [Theory] + [ConditionalTheory(nameof(PredefinedCulturesOnlyIsDisabled))] [MemberData(nameof(GetAscii_TestData))] public void GetAscii(string unicode, int index, int count, string expected) { @@ -1078,7 +1096,7 @@ public void GetAscii(string unicode, int index, int count, string expected) Assert.Equal(expected, new IdnMapping().GetAscii(unicode, index, count)); } - [Theory] + [ConditionalTheory(nameof(PredefinedCulturesOnlyIsDisabled))] [MemberData(nameof(GetUnicode_TestData))] public void GetUnicode(string ascii, int index, int count, string expected) { @@ -1093,7 +1111,7 @@ public void GetUnicode(string ascii, int index, int count, string expected) Assert.Equal(expected, new IdnMapping().GetUnicode(ascii, index, count)); } - [Fact] + [ConditionalFact(nameof(PredefinedCulturesOnlyIsDisabled))] public void TestHashing() { StringComparer cultureComparer = StringComparer.Create(CultureInfo.GetCultureInfo("tr-TR"), true); @@ -1102,7 +1120,7 @@ public void TestHashing() Assert.Equal(ordinalComparer.GetHashCode(turkishString), cultureComparer.GetHashCode(turkishString)); } - [Theory] + [ConditionalTheory(nameof(PredefinedCulturesOnlyIsDisabled))] [InlineData('a', 'A', 'a')] [InlineData('A', 'A', 'a')] [InlineData('i', 'I', 'i')] // to verify that we don't special-case the Turkish I in the invariant globalization mode @@ -1121,7 +1139,7 @@ public void TestRune(int original, int expectedToUpper, int expectedToLower) Assert.Equal(expectedToLower, Rune.ToLower(originalRune, CultureInfo.GetCultureInfo("tr-TR")).Value); } - [Fact] + [ConditionalFact(nameof(PredefinedCulturesOnlyIsDisabled))] public void TestGetCultureInfo_PredefinedOnly_ReturnsSame() { Assert.Equal(CultureInfo.GetCultureInfo("en-US"), CultureInfo.GetCultureInfo("en-US", predefinedOnly: true)); diff --git a/src/libraries/System.Globalization/tests/Invariant/runtimeconfig.template.json b/src/libraries/System.Globalization/tests/Invariant/runtimeconfig.template.json index dd404e151b8d6..076d849845574 100644 --- a/src/libraries/System.Globalization/tests/Invariant/runtimeconfig.template.json +++ b/src/libraries/System.Globalization/tests/Invariant/runtimeconfig.template.json @@ -1,5 +1,6 @@ { "configProperties": { - "System.Globalization.Invariant": true + "System.Globalization.Invariant": true, + "System.Globalization.PredefinedCulturesOnly": false } } \ No newline at end of file diff --git a/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.Substitutions.Shared.xml b/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.Substitutions.Shared.xml index ba2db281981b8..f7ca754bb6a92 100644 --- a/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.Substitutions.Shared.xml +++ b/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.Substitutions.Shared.xml @@ -10,6 +10,7 @@ to be trimmed when Invariant=true, and allows for the Settings static cctor (on Unix) to be preserved when Invariant=false. --> + diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index 7c80c23f63241..1669ffb9bbb59 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -957,6 +957,9 @@ Culture is not supported. + + Only the invariant culture is supported in globalization-invariant mode. See https://aka.ms/GlobalizationInvariantMode for more information. + Resolved assembly's simple name should be the same as of the requested assembly. diff --git a/src/libraries/System.Private.CoreLib/src/System/AppContextConfigHelper.cs b/src/libraries/System.Private.CoreLib/src/System/AppContextConfigHelper.cs index 331ef281bb32a..712f8973ca697 100644 --- a/src/libraries/System.Private.CoreLib/src/System/AppContextConfigHelper.cs +++ b/src/libraries/System.Private.CoreLib/src/System/AppContextConfigHelper.cs @@ -10,15 +10,12 @@ internal static class AppContextConfigHelper internal static bool GetBooleanConfig(string configName, bool defaultValue) => AppContext.TryGetSwitch(configName, out bool value) ? value : defaultValue; - internal static bool GetBooleanConfig(string switchName, string envVariable) + internal static bool GetBooleanConfig(string switchName, string envVariable, bool defaultValue = false) { if (!AppContext.TryGetSwitch(switchName, out bool ret)) { string? switchValue = Environment.GetEnvironmentVariable(envVariable); - if (switchValue != null) - { - ret = bool.IsTrueStringIgnoreCase(switchValue) || switchValue.Equals("1"); - } + ret = switchValue != null ? (bool.IsTrueStringIgnoreCase(switchValue) || switchValue.Equals("1")) : defaultValue; } return ret; diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs index 55e12c1148330..c952e4cb0cddd 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs @@ -671,8 +671,7 @@ private static CultureData CreateCultureWithInvariantData() if (GlobalizationMode.PredefinedCulturesOnly) { - Debug.Assert(!GlobalizationMode.Invariant); - if (GlobalizationMode.UseNls ? !NlsIsEnsurePredefinedLocaleName(cultureName): !IcuIsEnsurePredefinedLocaleName(cultureName)) + if (GlobalizationMode.Invariant || (GlobalizationMode.UseNls ? !NlsIsEnsurePredefinedLocaleName(cultureName): !IcuIsEnsurePredefinedLocaleName(cultureName))) return null; } @@ -865,7 +864,7 @@ internal static CultureData GetCultureData(int culture, bool bUseUserOverride) if (GlobalizationMode.Invariant) { // LCID is not supported in the InvariantMode - throw new CultureNotFoundException(nameof(culture), culture, SR.Argument_CultureNotSupported); + throw new CultureNotFoundException(nameof(culture), culture, SR.Argument_CultureNotSupportedInInvariantMode); } // Convert the lcid to a name, then use that diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureInfo.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureInfo.cs index 729119c8c163a..e862e03293bdc 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureInfo.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureInfo.cs @@ -158,6 +158,8 @@ private static CultureInfo InitializeUserDefaultUICulture() return s_userDefaultUICulture!; } + private static string GetCultureNotSupportedExceptionMessage() => GlobalizationMode.Invariant ? SR.Argument_CultureNotSupportedInInvariantMode : SR.Argument_CultureNotSupported; + public CultureInfo(string name) : this(name, true) { } @@ -174,7 +176,7 @@ public CultureInfo(string name, bool useUserOverride) if (cultureData == null) { - throw new CultureNotFoundException(nameof(name), name, SR.Argument_CultureNotSupported); + throw new CultureNotFoundException(nameof(name), name, GetCultureNotSupportedExceptionMessage()); } _cultureData = cultureData; @@ -249,7 +251,7 @@ internal CultureInfo(string cultureName, string textAndCompareCultureName) } CultureData? cultureData = CultureData.GetCultureData(cultureName, false) ?? - throw new CultureNotFoundException(nameof(cultureName), cultureName, SR.Argument_CultureNotSupported); + throw new CultureNotFoundException(nameof(cultureName), cultureName, GetCultureNotSupportedExceptionMessage()); _cultureData = cultureData; @@ -1060,7 +1062,7 @@ public static CultureInfo GetCultureInfo(int culture) } catch (ArgumentException) { - throw new CultureNotFoundException(nameof(culture), culture, SR.Argument_CultureNotSupported); + throw new CultureNotFoundException(nameof(culture), culture, GetCultureNotSupportedExceptionMessage()); } lock (lcidTable) @@ -1096,7 +1098,7 @@ public static CultureInfo GetCultureInfo(string name) } result = CreateCultureInfoNoThrow(name, useUserOverride: false) ?? - throw new CultureNotFoundException(nameof(name), name, SR.Argument_CultureNotSupported); + throw new CultureNotFoundException(nameof(name), name, GetCultureNotSupportedExceptionMessage()); result._isReadOnly = true; // Remember our name as constructed. Do NOT use alternate sort name versions because diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.cs index 5afa2a4e2b979..3536ed7d534e7 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.cs @@ -11,8 +11,8 @@ internal static partial class GlobalizationMode // split from GlobalizationMode so the whole class can be trimmed when Invariant=true. private static partial class Settings { - internal static readonly bool PredefinedCulturesOnly = AppContextConfigHelper.GetBooleanConfig("System.Globalization.PredefinedCulturesOnly", "DOTNET_SYSTEM_GLOBALIZATION_PREDEFINED_CULTURES_ONLY"); internal static bool Invariant { get; } = GetInvariantSwitchValue(); + internal static readonly bool PredefinedCulturesOnly = AppContextConfigHelper.GetBooleanConfig("System.Globalization.PredefinedCulturesOnly", "DOTNET_SYSTEM_GLOBALIZATION_PREDEFINED_CULTURES_ONLY", Invariant); } // Note: Invariant=true and Invariant=false are substituted at different levels in the ILLink.Substitutions file. @@ -20,7 +20,7 @@ private static partial class Settings // static cctor (on Unix) to be preserved when Invariant=false. internal static bool Invariant => Settings.Invariant; - internal static bool PredefinedCulturesOnly => !Invariant && Settings.PredefinedCulturesOnly; + internal static bool PredefinedCulturesOnly => Settings.PredefinedCulturesOnly; private static bool GetInvariantSwitchValue() => AppContextConfigHelper.GetBooleanConfig("System.Globalization.Invariant", "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"); diff --git a/src/libraries/System.Private.CoreLib/src/System/Resources/ManifestBasedResourceGroveler.cs b/src/libraries/System.Private.CoreLib/src/System/Resources/ManifestBasedResourceGroveler.cs index 18b714a42ce38..c940600ee8c25 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Resources/ManifestBasedResourceGroveler.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Resources/ManifestBasedResourceGroveler.cs @@ -139,7 +139,7 @@ internal static CultureInfo GetNeutralResourcesLanguage(Assembly a, out Ultimate Debug.Assert(a != null, "assembly != null"); NeutralResourcesLanguageAttribute? attr = a.GetCustomAttribute(); - if (attr == null) + if (attr == null || (GlobalizationMode.Invariant && GlobalizationMode.PredefinedCulturesOnly)) { fallbackLocation = UltimateResourceFallbackLocation.MainAssembly; return CultureInfo.InvariantCulture; diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Win32.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Win32.cs index 93c9070d53924..30862efd02bb8 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Win32.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Win32.cs @@ -848,15 +848,15 @@ private static unsafe string GetFileMuiPath(string filePath, CultureInfo culture /// If a localized resource file exists, we LoadString resource ID "123" and /// return it to our caller. /// - private static string GetLocalizedNameByMuiNativeResource(string resource, CultureInfo? cultureInfo = null) + private static string GetLocalizedNameByMuiNativeResource(string resource) { - if (string.IsNullOrEmpty(resource)) + if (string.IsNullOrEmpty(resource) || (GlobalizationMode.Invariant && GlobalizationMode.PredefinedCulturesOnly)) { return string.Empty; } // Use the current UI culture when culture not specified - cultureInfo ??= CultureInfo.CurrentUICulture; + CultureInfo cultureInfo = CultureInfo.CurrentUICulture; // parse "@tzres.dll, -100" // diff --git a/src/libraries/System.Runtime/tests/TrimmingTests/InvariantGlobalizationTrue.cs b/src/libraries/System.Runtime/tests/TrimmingTests/InvariantGlobalizationTrue.cs index 7daa557239f3d..6b16a662c504a 100644 --- a/src/libraries/System.Runtime/tests/TrimmingTests/InvariantGlobalizationTrue.cs +++ b/src/libraries/System.Runtime/tests/TrimmingTests/InvariantGlobalizationTrue.cs @@ -4,7 +4,6 @@ using System; using System.Globalization; using System.Reflection; -using System.Threading; /// /// Ensures setting InvariantGlobalization = true still works in a trimmed app. @@ -13,18 +12,24 @@ class Program { static int Main(string[] args) { - // since we are using Invariant GlobalizationMode = true, setting the culture doesn't matter. - // The app will always use Invariant mode, so even in the Turkish culture, 'i' ToUpper will be "I" - Thread.CurrentThread.CurrentCulture = new CultureInfo("tr-TR"); + // Ensure the internal GlobalizationMode class is trimmed correctly + Type globalizationMode = GetCoreLibType("System.Globalization.GlobalizationMode"); + const BindingFlags allStatics = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static; + + try + { + CultureInfo.CurrentCulture = new CultureInfo("tr-TR"); + return -1; // we expect new CultureInfo("tr-TR") to throw. + } + catch (CultureNotFoundException) + { + } + if ("i".ToUpper() != "I") { - // 'i' ToUpper was not "I", so fail - return -1; + return -3; } - // Ensure the internal GlobalizationMode class is trimmed correctly - Type globalizationMode = GetCoreLibType("System.Globalization.GlobalizationMode"); - const BindingFlags allStatics = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static; foreach (MemberInfo member in globalizationMode.GetMembers(allStatics)) { // properties and their backing getter methods are OK @@ -44,7 +49,7 @@ static int Main(string[] args) // Some unexpected member was left on GlobalizationMode, fail Console.WriteLine($"Member '{member.Name}' was not trimmed from GlobalizationMode, but should have been."); - return -2; + return -4; } return 100; diff --git a/src/libraries/System.Runtime/tests/TrimmingTests/System.Runtime.TrimmingTests.proj b/src/libraries/System.Runtime/tests/TrimmingTests/System.Runtime.TrimmingTests.proj index 02b4882782e96..2f2da7a4e1205 100644 --- a/src/libraries/System.Runtime/tests/TrimmingTests/System.Runtime.TrimmingTests.proj +++ b/src/libraries/System.Runtime/tests/TrimmingTests/System.Runtime.TrimmingTests.proj @@ -15,7 +15,7 @@ --feature System.Globalization.Invariant false - --feature System.Globalization.Invariant true + --feature System.Globalization.Invariant true --feature System.Globalization.PredefinedCulturesOnly true