Skip to content

Commit 04dac7b

Browse files
tarekgheerhardt
andauthored
Allow restricting cultures creation with any arbitrary names in Globalization Invariant Mode (#54247)
Co-authored-by: Eric Erhardt <eric.erhardt@microsoft.com>
1 parent be80f67 commit 04dac7b

File tree

15 files changed

+167
-56
lines changed

15 files changed

+167
-56
lines changed

docs/workflow/trimming/feature-switches.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ configurations but their defaults might vary as any SDK can set the defaults dif
1313
| EnableUnsafeBinaryFormatterSerialization | System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization | BinaryFormatter serialization support is trimmed when set to false |
1414
| EventSourceSupport | System.Diagnostics.Tracing.EventSource.IsSupported | Any EventSource related code or logic is trimmed when set to false |
1515
| InvariantGlobalization | System.Globalization.Invariant | All globalization specific code and data is trimmed when set to true |
16+
| PredefinedCulturesOnly | System.Globalization.PredefinedCulturesOnly | Don't allow creating a culture for which the platform does not have data |
1617
| UseSystemResourceKeys | System.Resources.UseSystemResourceKeys | Any localizable resources for system assemblies is trimmed when set to true |
1718
| HttpActivityPropagationSupport | System.Net.Http.EnableActivityPropagation | Any dependency related to diagnostics support for System.Net.Http is trimmed when set to false |
1819
| UseNativeHttpHandler | System.Net.Http.UseNativeHttpHandler | HttpClient uses by default platform native implementation of HttpMessageHandler if set to true. |

src/libraries/System.Globalization/tests/CultureInfo/GetCultureInfo.cs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Collections.Generic;
55
using Microsoft.DotNet.RemoteExecutor;
66
using System.Diagnostics;
7+
using System.Collections.Concurrent;
78
using Xunit;
89

910
namespace System.Globalization.Tests
@@ -139,5 +140,80 @@ public void PredefinedCulturesOnlyEnvVarTest(string predefinedCulturesOnlyEnvVar
139140
}
140141
}, cultureName, predefinedCulturesOnlyEnvVar, new RemoteInvokeOptions { StartInfo = psi }).Dispose();
141142
}
143+
144+
[ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
145+
[InlineData(true, true, false)]
146+
[InlineData(true, true, true)]
147+
[InlineData(true, false, true)]
148+
[InlineData(false, true, true)]
149+
[InlineData(false, true, false)]
150+
[InlineData(false, false, true)]
151+
public void TestAllowInvariantCultureOnly(bool enableInvariant, bool predefinedCulturesOnly, bool declarePredefinedCulturesOnly)
152+
{
153+
var psi = new ProcessStartInfo();
154+
psi.Environment.Clear();
155+
156+
if (enableInvariant)
157+
{
158+
psi.Environment.Add("DOTNET_SYSTEM_GLOBALIZATION_INVARIANT", "true");
159+
}
160+
161+
if (declarePredefinedCulturesOnly)
162+
{
163+
psi.Environment.Add("DOTNET_SYSTEM_GLOBALIZATION_PREDEFINED_CULTURES_ONLY", predefinedCulturesOnly ? "true" : "false");
164+
}
165+
166+
bool restricted = enableInvariant && (declarePredefinedCulturesOnly ? predefinedCulturesOnly : true);
167+
168+
RemoteExecutor.Invoke((invariantEnabled, isRestricted) =>
169+
{
170+
bool restrictedMode = bool.Parse(isRestricted);
171+
172+
// First ensure we can create the current cultures regardless of the mode we are in
173+
Assert.NotNull(CultureInfo.CurrentCulture);
174+
Assert.NotNull(CultureInfo.CurrentUICulture);
175+
176+
// Invariant culture should be valid all the time
177+
Assert.Equal("", new CultureInfo("").Name);
178+
Assert.Equal("", CultureInfo.InvariantCulture.Name);
179+
180+
if (restrictedMode)
181+
{
182+
Assert.Equal("", CultureInfo.CurrentCulture.Name);
183+
Assert.Equal("", CultureInfo.CurrentUICulture.Name);
184+
185+
// Throwing exception is testing accessing the resources in this restricted mode.
186+
// We should retrieve the resources from the neutral resources in the main assemblies.
187+
AssertExtensions.Throws<CultureNotFoundException>(() => new CultureInfo("en-US"));
188+
AssertExtensions.Throws<CultureNotFoundException>(() => new CultureInfo("en"));
189+
190+
AssertExtensions.Throws<CultureNotFoundException>(() => new CultureInfo("ja-JP"));
191+
AssertExtensions.Throws<CultureNotFoundException>(() => new CultureInfo("es"));
192+
193+
// Test throwing exceptions from non-core assemblies.
194+
Exception exception = Record.Exception(() => new ConcurrentBag<string>(null));
195+
Assert.NotNull(exception);
196+
Assert.IsType<ArgumentNullException>(exception);
197+
Assert.Equal("collection", (exception as ArgumentNullException).ParamName);
198+
Assert.Equal("The collection argument is null. (Parameter 'collection')", exception.Message);
199+
}
200+
else
201+
{
202+
Assert.Equal("en-US", new CultureInfo("en-US").Name);
203+
Assert.Equal("ja-JP", new CultureInfo("ja-JP").Name);
204+
Assert.Equal("en", new CultureInfo("en").Name);
205+
Assert.Equal("es", new CultureInfo("es").Name);
206+
}
207+
208+
// Ensure the Invariant Mode functionality still work
209+
if (bool.Parse(invariantEnabled))
210+
{
211+
Assert.True(CultureInfo.CurrentCulture.Calendar is GregorianCalendar);
212+
Assert.True("abcd".Equals("ABCD", StringComparison.CurrentCultureIgnoreCase));
213+
Assert.Equal("Invariant Language (Invariant Country)", CultureInfo.CurrentCulture.NativeName);
214+
}
215+
216+
}, enableInvariant.ToString(), restricted.ToString(), new RemoteInvokeOptions { StartInfo = psi }).Dispose();
217+
}
142218
}
143219
}

src/libraries/System.Globalization/tests/Invariant/InvariantMode.cs

Lines changed: 42 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Buffers;
5+
using System.Reflection;
56
using System.Buffers.Binary;
67
using System.Collections.Generic;
78
using System.IO;
@@ -13,6 +14,23 @@ namespace System.Globalization.Tests
1314
{
1415
public class InvariantModeTests
1516
{
17+
private static bool PredefinedCulturesOnlyIsDisabled { get; } = !PredefinedCulturesOnly();
18+
private static bool PredefinedCulturesOnly()
19+
{
20+
bool ret;
21+
22+
try
23+
{
24+
ret = (bool) typeof(object).Assembly.GetType("System.Globalization.GlobalizationMode").GetProperty("PredefinedCulturesOnly", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null);
25+
}
26+
catch
27+
{
28+
ret = false;
29+
}
30+
31+
return ret;
32+
}
33+
1634
public static IEnumerable<object[]> Cultures_TestData()
1735
{
1836
yield return new object[] { "en-US" };
@@ -490,13 +508,13 @@ public static IEnumerable<object[]> GetUnicode_TestData()
490508
yield return new object[] { "xn--de-jg4avhby1noc0d", 0, 21, "\u30D1\u30D5\u30A3\u30FC\u0064\u0065\u30EB\u30F3\u30D0" };
491509
}
492510

493-
[Fact]
511+
[ConditionalFact(nameof(PredefinedCulturesOnlyIsDisabled))]
494512
public static void IcuShouldNotBeLoaded()
495513
{
496514
Assert.False(PlatformDetection.IsIcuGlobalization);
497515
}
498516

499-
[Theory]
517+
[ConditionalTheory(nameof(PredefinedCulturesOnlyIsDisabled))]
500518
[MemberData(nameof(Cultures_TestData))]
501519
public void TestCultureData(string cultureName)
502520
{
@@ -636,7 +654,7 @@ public void TestCultureData(string cultureName)
636654
Assert.True(cultureName.Equals(ci.CompareInfo.Name, StringComparison.OrdinalIgnoreCase));
637655
}
638656

639-
[Theory]
657+
[ConditionalTheory(nameof(PredefinedCulturesOnlyIsDisabled))]
640658
[MemberData(nameof(Cultures_TestData))]
641659
public void SetCultureData(string cultureName)
642660
{
@@ -652,13 +670,13 @@ public void SetCultureData(string cultureName)
652670
Assert.Throws<ArgumentOutOfRangeException>(() => ci.DateTimeFormat.Calendar = new TaiwanCalendar());
653671
}
654672

655-
[Fact]
673+
[ConditionalFact(nameof(PredefinedCulturesOnlyIsDisabled))]
656674
public void TestEnum()
657675
{
658676
Assert.Equal(new CultureInfo[1] { CultureInfo.InvariantCulture }, CultureInfo.GetCultures(CultureTypes.AllCultures));
659677
}
660678

661-
[Theory]
679+
[ConditionalTheory(nameof(PredefinedCulturesOnlyIsDisabled))]
662680
[MemberData(nameof(Cultures_TestData))]
663681
public void TestSortVersion(string cultureName)
664682
{
@@ -670,7 +688,7 @@ public void TestSortVersion(string cultureName)
670688
Assert.Equal(version, new CultureInfo(cultureName).CompareInfo.Version);
671689
}
672690

673-
[Theory]
691+
[ConditionalTheory(nameof(PredefinedCulturesOnlyIsDisabled))]
674692
[InlineData(0, 0)]
675693
[InlineData(1, 2)]
676694
[InlineData(100_000, 200_000)]
@@ -683,7 +701,7 @@ public void TestGetSortKeyLength_Valid(int inputLength, int expectedSortKeyLengt
683701
Assert.Equal(expectedSortKeyLength, CultureInfo.InvariantCulture.CompareInfo.GetSortKeyLength(dummySpan));
684702
}
685703

686-
[Theory]
704+
[ConditionalTheory(nameof(PredefinedCulturesOnlyIsDisabled))]
687705
[InlineData(0x4000_0000)]
688706
[InlineData(int.MaxValue)]
689707
public unsafe void TestGetSortKeyLength_OverlongArgument(int inputLength)
@@ -698,7 +716,7 @@ public unsafe void TestGetSortKeyLength_OverlongArgument(int inputLength)
698716
});
699717
}
700718

701-
[Theory]
719+
[ConditionalTheory(nameof(PredefinedCulturesOnlyIsDisabled))]
702720
[InlineData("Hello", CompareOptions.None, "Hello")]
703721
[InlineData("Hello", CompareOptions.IgnoreWidth, "Hello")]
704722
[InlineData("Hello", CompareOptions.IgnoreCase, "HELLO")]
@@ -741,7 +759,7 @@ public unsafe void TestSortKey_FromSpan(string input, CompareOptions options, st
741759
}
742760
}
743761

744-
[Fact]
762+
[ConditionalFact(nameof(PredefinedCulturesOnlyIsDisabled))]
745763
public void TestSortKey_ZeroWeightCodePoints()
746764
{
747765
// In the invariant globalization mode, there's no such thing as a zero-weight code point,
@@ -753,7 +771,7 @@ public void TestSortKey_ZeroWeightCodePoints()
753771
Assert.NotEqual(0, SortKey.Compare(sortKeyForEmptyString, sortKeyForZeroWidthJoiner));
754772
}
755773

756-
[Theory]
774+
[ConditionalTheory(nameof(PredefinedCulturesOnlyIsDisabled))]
757775
[InlineData("", "", 0)]
758776
[InlineData("", "not-empty", -1)]
759777
[InlineData("not-empty", "", 1)]
@@ -794,7 +812,7 @@ private static StringComparison GetStringComparison(CompareOptions options)
794812
return sc;
795813
}
796814

797-
[Theory]
815+
[ConditionalTheory(nameof(PredefinedCulturesOnlyIsDisabled))]
798816
[MemberData(nameof(IndexOf_TestData))]
799817
public void TestIndexOf(string source, string value, int startIndex, int count, CompareOptions options, int result)
800818
{
@@ -841,7 +859,7 @@ static void TestCore(CompareInfo compareInfo, string source, string value, int s
841859
}
842860
}
843861

844-
[Theory]
862+
[ConditionalTheory(nameof(PredefinedCulturesOnlyIsDisabled))]
845863
[MemberData(nameof(LastIndexOf_TestData))]
846864
public void TestLastIndexOf(string source, string value, int startIndex, int count, CompareOptions options, int result)
847865
{
@@ -901,7 +919,7 @@ static void TestCore(CompareInfo compareInfo, string source, string value, int s
901919
}
902920
}
903921

904-
[Theory]
922+
[ConditionalTheory(nameof(PredefinedCulturesOnlyIsDisabled))]
905923
[MemberData(nameof(IsPrefix_TestData))]
906924
public void TestIsPrefix(string source, string value, CompareOptions options, bool result)
907925
{
@@ -936,7 +954,7 @@ public void TestIsPrefix(string source, string value, CompareOptions options, bo
936954
}
937955
}
938956

939-
[Theory]
957+
[ConditionalTheory(nameof(PredefinedCulturesOnlyIsDisabled))]
940958
[MemberData(nameof(IsSuffix_TestData))]
941959
public void TestIsSuffix(string source, string value, CompareOptions options, bool result)
942960
{
@@ -971,7 +989,7 @@ public void TestIsSuffix(string source, string value, CompareOptions options, bo
971989
}
972990
}
973991

974-
[Theory]
992+
[ConditionalTheory(nameof(PredefinedCulturesOnlyIsDisabled))]
975993
[InlineData("", false)]
976994
[InlineData('x', true)]
977995
[InlineData('\ud800', true)] // standalone high surrogate
@@ -988,7 +1006,7 @@ public void TestIsSortable(object sourceObj, bool expectedResult)
9881006
}
9891007
}
9901008

991-
[Theory]
1009+
[ConditionalTheory(nameof(PredefinedCulturesOnlyIsDisabled))]
9921010
[MemberData(nameof(Compare_TestData))]
9931011
public void TestCompare(string source, string value, CompareOptions options, int result)
9941012
{
@@ -1019,7 +1037,7 @@ public void TestCompare(string source, string value, CompareOptions options, int
10191037
}
10201038

10211039

1022-
[Theory]
1040+
[ConditionalTheory(nameof(PredefinedCulturesOnlyIsDisabled))]
10231041
[MemberData(nameof(ToLower_TestData))]
10241042
public void TestToLower(string upper, string lower, bool result)
10251043
{
@@ -1030,7 +1048,7 @@ public void TestToLower(string upper, string lower, bool result)
10301048
}
10311049
}
10321050

1033-
[Theory]
1051+
[ConditionalTheory(nameof(PredefinedCulturesOnlyIsDisabled))]
10341052
[MemberData(nameof(ToUpper_TestData))]
10351053
public void TestToUpper(string lower, string upper, bool result)
10361054
{
@@ -1041,7 +1059,7 @@ public void TestToUpper(string lower, string upper, bool result)
10411059
}
10421060
}
10431061

1044-
[Theory]
1062+
[ConditionalTheory(nameof(PredefinedCulturesOnlyIsDisabled))]
10451063
[InlineData("", NormalizationForm.FormC)]
10461064
[InlineData("\uFB01", NormalizationForm.FormC)]
10471065
[InlineData("\uFB01", NormalizationForm.FormD)]
@@ -1063,7 +1081,7 @@ public void TestNormalization(string s, NormalizationForm form)
10631081
Assert.Equal(s, s.Normalize(form));
10641082
}
10651083

1066-
[Theory]
1084+
[ConditionalTheory(nameof(PredefinedCulturesOnlyIsDisabled))]
10671085
[MemberData(nameof(GetAscii_TestData))]
10681086
public void GetAscii(string unicode, int index, int count, string expected)
10691087
{
@@ -1078,7 +1096,7 @@ public void GetAscii(string unicode, int index, int count, string expected)
10781096
Assert.Equal(expected, new IdnMapping().GetAscii(unicode, index, count));
10791097
}
10801098

1081-
[Theory]
1099+
[ConditionalTheory(nameof(PredefinedCulturesOnlyIsDisabled))]
10821100
[MemberData(nameof(GetUnicode_TestData))]
10831101
public void GetUnicode(string ascii, int index, int count, string expected)
10841102
{
@@ -1093,7 +1111,7 @@ public void GetUnicode(string ascii, int index, int count, string expected)
10931111
Assert.Equal(expected, new IdnMapping().GetUnicode(ascii, index, count));
10941112
}
10951113

1096-
[Fact]
1114+
[ConditionalFact(nameof(PredefinedCulturesOnlyIsDisabled))]
10971115
public void TestHashing()
10981116
{
10991117
StringComparer cultureComparer = StringComparer.Create(CultureInfo.GetCultureInfo("tr-TR"), true);
@@ -1102,7 +1120,7 @@ public void TestHashing()
11021120
Assert.Equal(ordinalComparer.GetHashCode(turkishString), cultureComparer.GetHashCode(turkishString));
11031121
}
11041122

1105-
[Theory]
1123+
[ConditionalTheory(nameof(PredefinedCulturesOnlyIsDisabled))]
11061124
[InlineData('a', 'A', 'a')]
11071125
[InlineData('A', 'A', 'a')]
11081126
[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)
11211139
Assert.Equal(expectedToLower, Rune.ToLower(originalRune, CultureInfo.GetCultureInfo("tr-TR")).Value);
11221140
}
11231141

1124-
[Fact]
1142+
[ConditionalFact(nameof(PredefinedCulturesOnlyIsDisabled))]
11251143
public void TestGetCultureInfo_PredefinedOnly_ReturnsSame()
11261144
{
11271145
Assert.Equal(CultureInfo.GetCultureInfo("en-US"), CultureInfo.GetCultureInfo("en-US", predefinedOnly: true));
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"configProperties": {
3-
"System.Globalization.Invariant": true
3+
"System.Globalization.Invariant": true,
4+
"System.Globalization.PredefinedCulturesOnly": false
45
}
56
}

src/libraries/System.Private.CoreLib/src/ILLink/ILLink.Substitutions.Shared.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
to be trimmed when Invariant=true, and allows for the Settings static cctor (on Unix) to be preserved when Invariant=false. -->
1111
<type fullname="System.Globalization.GlobalizationMode">
1212
<method signature="System.Boolean get_Invariant()" body="stub" value="true" feature="System.Globalization.Invariant" featurevalue="true" />
13+
<method signature="System.Boolean get_PredefinedCulturesOnly()" body="stub" value="true" feature="System.Globalization.PredefinedCulturesOnly" featurevalue="true" />
1314
</type>
1415
<type fullname="System.Globalization.GlobalizationMode/Settings">
1516
<method signature="System.Boolean get_Invariant()" body="stub" value="false" feature="System.Globalization.Invariant" featurevalue="false" />

src/libraries/System.Private.CoreLib/src/Resources/Strings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -957,6 +957,9 @@
957957
<data name="Argument_CultureNotSupported" xml:space="preserve">
958958
<value>Culture is not supported.</value>
959959
</data>
960+
<data name="Argument_CultureNotSupportedInInvariantMode" xml:space="preserve">
961+
<value>Only the invariant culture is supported in globalization-invariant mode. See https://aka.ms/GlobalizationInvariantMode for more information.</value>
962+
</data>
960963
<data name="Argument_CustomAssemblyLoadContextRequestedNameMismatch" xml:space="preserve">
961964
<value>Resolved assembly's simple name should be the same as of the requested assembly.</value>
962965
</data>

src/libraries/System.Private.CoreLib/src/System/AppContextConfigHelper.cs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,12 @@ internal static class AppContextConfigHelper
1010
internal static bool GetBooleanConfig(string configName, bool defaultValue) =>
1111
AppContext.TryGetSwitch(configName, out bool value) ? value : defaultValue;
1212

13-
internal static bool GetBooleanConfig(string switchName, string envVariable)
13+
internal static bool GetBooleanConfig(string switchName, string envVariable, bool defaultValue = false)
1414
{
1515
if (!AppContext.TryGetSwitch(switchName, out bool ret))
1616
{
1717
string? switchValue = Environment.GetEnvironmentVariable(envVariable);
18-
if (switchValue != null)
19-
{
20-
ret = bool.IsTrueStringIgnoreCase(switchValue) || switchValue.Equals("1");
21-
}
18+
ret = switchValue != null ? (bool.IsTrueStringIgnoreCase(switchValue) || switchValue.Equals("1")) : defaultValue;
2219
}
2320

2421
return ret;

0 commit comments

Comments
 (0)