Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow restricting cultures creation with any arbitrary names in Globalization Invariant Mode #54247

Merged
merged 15 commits into from
Jul 1, 2021
Merged
1 change: 1 addition & 0 deletions docs/workflow/trimming/feature-switches.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
tarekgh marked this conversation as resolved.
Show resolved Hide resolved
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<CultureNotFoundException>(() => new CultureInfo("en-US"));
AssertExtensions.Throws<CultureNotFoundException>(() => new CultureInfo("en"));

AssertExtensions.Throws<CultureNotFoundException>(() => new CultureInfo("ja-JP"));
AssertExtensions.Throws<CultureNotFoundException>(() => new CultureInfo("es"));

// Test throwing exceptions from non-core assemblies.
Exception exception = Record.Exception(() => new ConcurrentBag<string>(null));
Assert.NotNull(exception);
Assert.IsType<ArgumentNullException>(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();
}
}
}
66 changes: 42 additions & 24 deletions src/libraries/System.Globalization/tests/Invariant/InvariantMode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<object[]> Cultures_TestData()
{
yield return new object[] { "en-US" };
Expand Down Expand Up @@ -490,13 +508,13 @@ public static IEnumerable<object[]> GetUnicode_TestData()
yield return new object[] { "xn--de-jg4avhby1noc0d", 0, 21, "\u30D1\u30D5\u30A3\u30FC\u0064\u0065\u30EB\u30F3\u30D0" };
}

[Fact]
[ConditionalFact(nameof(PredefinedCulturesOnlyIsDisabled))]
eerhardt marked this conversation as resolved.
Show resolved Hide resolved
public static void IcuShouldNotBeLoaded()
{
Assert.False(PlatformDetection.IsIcuGlobalization);
}

[Theory]
[ConditionalTheory(nameof(PredefinedCulturesOnlyIsDisabled))]
[MemberData(nameof(Cultures_TestData))]
public void TestCultureData(string cultureName)
{
Expand Down Expand Up @@ -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)
{
Expand All @@ -652,13 +670,13 @@ public void SetCultureData(string cultureName)
Assert.Throws<ArgumentOutOfRangeException>(() => 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)
{
Expand All @@ -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)]
Expand All @@ -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)
Expand All @@ -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")]
Expand Down Expand Up @@ -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,
Expand All @@ -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)]
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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
Expand All @@ -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)
{
Expand Down Expand Up @@ -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)
{
Expand All @@ -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)
{
Expand All @@ -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)]
Expand All @@ -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)
{
Expand All @@ -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)
{
Expand All @@ -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);
Expand All @@ -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
Expand All @@ -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));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"configProperties": {
"System.Globalization.Invariant": true
"System.Globalization.Invariant": true,
"System.Globalization.PredefinedCulturesOnly": false
tarekgh marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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. -->
<type fullname="System.Globalization.GlobalizationMode">
<method signature="System.Boolean get_Invariant()" body="stub" value="true" feature="System.Globalization.Invariant" featurevalue="true" />
<method signature="System.Boolean get_PredefinedCulturesOnly()" body="stub" value="true" feature="System.Globalization.PredefinedCulturesOnly" featurevalue="true" />
tarekgh marked this conversation as resolved.
Show resolved Hide resolved
</type>
<type fullname="System.Globalization.GlobalizationMode/Settings">
<method signature="System.Boolean get_Invariant()" body="stub" value="false" feature="System.Globalization.Invariant" featurevalue="false" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -957,6 +957,9 @@
<data name="Argument_CultureNotSupported" xml:space="preserve">
<value>Culture is not supported.</value>
</data>
<data name="Argument_CultureNotSupportedInInvariantMode" xml:space="preserve">
<value>Only the invariant culture is supported in globalization-invariant mode. See https://aka.ms/GlobalizationInvariantMode for more information.</value>
</data>
<data name="Argument_CustomAssemblyLoadContextRequestedNameMismatch" xml:space="preserve">
<value>Resolved assembly's simple name should be the same as of the requested assembly.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading