From 6e6eab28abba9a9626310bacafe25870767d2c8b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 9 Mar 2023 18:13:22 -0600 Subject: [PATCH] [release/7.0] Fix thread-safety issues with enumerating ResourceManager. (#81283) * Fix thread-safety issues with enumerating ResourceManager. Concurrently enumerating a ResourceManager while also calling GetString() and similar methods was prone to both transient errors and deadlock. The transient errors were caused by RuntimeResourceSet calling internal methods on ResourceReader that did not properly lock. Now, all exposed methods on the Reader are thread-safe. The deadlock was called by inconsistent lock ordering between ResourceReader.ResourceEnumerator and RuntimeResourceSet which both lock on the RuntimeResourceSet's cache and on the ResourceReader itself. Now, the enumerator does not need to take both locks at the same time. Fix #74052 Fix #74868 * Remove trailing whitespace * Address feedback from https://github.com/dotnet/runtime/pull/75054 * Add comment in response to https://github.com/dotnet/runtime/pull/75054#issuecomment-1317470280 * Implement feedback from https://github.com/dotnet/runtime/pull/75054 * Increase timeout for TestResourceManagerIsSafeForConcurrentAccessAndEnumeration (#80330) This raises the timeout to 30s, the same as what we have for the equivalent ResourceManager test (https://github.com/dotnet/runtime/blob/15fcb990fe17348ab6ddde0939200b900169920b/src/libraries/System.Resources.ResourceManager/tests/ResourceManagerTests.cs#L255). fix #80277 --------- Co-authored-by: Michael Adelson Co-authored-by: madelson <1269046+madelson@users.noreply.github.com> Co-authored-by: Buyaa Namnan --- .../src/System/Resources/ResourceReader.cs | 156 +++++---- .../System/Resources/RuntimeResourceSet.cs | 11 +- .../tests/BinaryResourceWriterUnitTest.cs | 48 ++- .../tests/ResourceManagerTests.cs | 49 +++ .../tests/Resources/AToZResx.Designer.cs | 297 ++++++++++++++++++ .../tests/Resources/AToZResx.resx | 198 ++++++++++++ ...tem.Resources.ResourceManager.Tests.csproj | 35 +-- 7 files changed, 704 insertions(+), 90 deletions(-) create mode 100644 src/libraries/System.Resources.ResourceManager/tests/Resources/AToZResx.Designer.cs create mode 100644 src/libraries/System.Resources.ResourceManager/tests/Resources/AToZResx.resx diff --git a/src/libraries/System.Private.CoreLib/src/System/Resources/ResourceReader.cs b/src/libraries/System.Private.CoreLib/src/System/Resources/ResourceReader.cs index 7d8a33c091b35..b2f9e9622a2a1 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Resources/ResourceReader.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Resources/ResourceReader.cs @@ -9,6 +9,7 @@ using System.IO; using System.Runtime.CompilerServices; using System.Text; +using System.Threading; namespace System.Resources #if RESOURCES_EXTENSIONS @@ -60,10 +61,11 @@ public sealed partial class // it make sense to use anything less than one page? private const int DefaultFileStreamBufferSize = 4096; - private BinaryReader _store; // backing store we're reading from. - // Used by RuntimeResourceSet and this class's enumerator. Maps - // resource name to a value, a ResourceLocator, or a - // LooselyLinkedManifestResource. + // Backing store we're reading from. Usages outside of constructor + // initialization must be protected by lock (this). + private BinaryReader _store; + // Used by RuntimeResourceSet and this class's enumerator. + // Accesses must be protected by lock(_resCache). internal Dictionary? _resCache; private long _nameSectionOffset; // Offset to name section of file. private long _dataSectionOffset; // Offset to Data section of file. @@ -88,7 +90,6 @@ public sealed partial class // Version number of .resources file, for compatibility private int _version; - public #if RESOURCES_EXTENSIONS DeserializingResourceReader(string fileName) @@ -169,13 +170,16 @@ private unsafe void Dispose(bool disposing) } } - internal static unsafe int ReadUnalignedI4(int* p) + private static unsafe int ReadUnalignedI4(int* p) { return BinaryPrimitives.ReadInt32LittleEndian(new ReadOnlySpan(p, sizeof(int))); } private void SkipString() { + // Note: this method assumes that it is called either during object + // construction or within another method that locks on this. + int stringLength = _store.Read7BitEncodedInt(); if (stringLength < 0) { @@ -234,6 +238,7 @@ public IDictionaryEnumerator GetEnumerator() return new ResourceEnumerator(this); } + // Called from RuntimeResourceSet internal ResourceEnumerator GetEnumeratorInternal() { return new ResourceEnumerator(this); @@ -243,6 +248,7 @@ internal ResourceEnumerator GetEnumeratorInternal() // To read the data, seek to _dataSectionOffset + dataPos, then // read the resource type & data. // This does a binary search through the names. + // Called from RuntimeResourceSet internal int FindPosForResource(string name) { Debug.Assert(_store != null, "ResourceReader is closed!"); @@ -327,6 +333,8 @@ internal int FindPosForResource(string name) private unsafe bool CompareStringEqualsName(string name) { Debug.Assert(_store != null, "ResourceReader is closed!"); + Debug.Assert(Monitor.IsEntered(this)); // uses _store + int byteLen = _store.Read7BitEncodedInt(); if (byteLen < 0) { @@ -459,68 +467,74 @@ private unsafe string AllocateStringForNameIndex(int index, out int dataOffset) } // This takes a virtual offset into the data section and reads a String - // from that location. - // Anyone who calls LoadObject should make sure they take a lock so - // no one can cause us to do a seek in here. + // from that location. Called from RuntimeResourceSet internal string? LoadString(int pos) { Debug.Assert(_store != null, "ResourceReader is closed!"); - _store.BaseStream.Seek(_dataSectionOffset + pos, SeekOrigin.Begin); - string? s = null; - int typeIndex = _store.Read7BitEncodedInt(); - if (_version == 1) - { - if (typeIndex == -1) - return null; - if (FindType(typeIndex) != typeof(string)) - throw new InvalidOperationException(SR.Format(SR.InvalidOperation_ResourceNotString_Type, FindType(typeIndex).FullName)); - s = _store.ReadString(); - } - else + + lock (this) { - ResourceTypeCode typeCode = (ResourceTypeCode)typeIndex; - if (typeCode != ResourceTypeCode.String && typeCode != ResourceTypeCode.Null) + _store.BaseStream.Seek(_dataSectionOffset + pos, SeekOrigin.Begin); + string? s = null; + int typeIndex = _store.Read7BitEncodedInt(); + if (_version == 1) { - string? typeString; - if (typeCode < ResourceTypeCode.StartOfUserTypes) - typeString = typeCode.ToString(); - else - typeString = FindType(typeCode - ResourceTypeCode.StartOfUserTypes).FullName; - throw new InvalidOperationException(SR.Format(SR.InvalidOperation_ResourceNotString_Type, typeString)); - } - if (typeCode == ResourceTypeCode.String) // ignore Null + if (typeIndex == -1) + return null; + if (FindType(typeIndex) != typeof(string)) + throw new InvalidOperationException(SR.Format(SR.InvalidOperation_ResourceNotString_Type, FindType(typeIndex).FullName)); s = _store.ReadString(); + } + else + { + ResourceTypeCode typeCode = (ResourceTypeCode)typeIndex; + if (typeCode != ResourceTypeCode.String && typeCode != ResourceTypeCode.Null) + { + string? typeString; + if (typeCode < ResourceTypeCode.StartOfUserTypes) + typeString = typeCode.ToString(); + else + typeString = FindType(typeCode - ResourceTypeCode.StartOfUserTypes).FullName; + throw new InvalidOperationException(SR.Format(SR.InvalidOperation_ResourceNotString_Type, typeString)); + } + if (typeCode == ResourceTypeCode.String) // ignore Null + s = _store.ReadString(); + } + return s; } - return s; } // Called from RuntimeResourceSet internal object? LoadObject(int pos) { - if (_version == 1) - return LoadObjectV1(pos); - return LoadObjectV2(pos, out _); + lock (this) + { + return _version == 1 ? LoadObjectV1(pos) : LoadObjectV2(pos, out _); + } } + // Called from RuntimeResourceSet internal object? LoadObject(int pos, out ResourceTypeCode typeCode) { - if (_version == 1) + lock (this) { - object? o = LoadObjectV1(pos); - typeCode = (o is string) ? ResourceTypeCode.String : ResourceTypeCode.StartOfUserTypes; - return o; + if (_version == 1) + { + object? o = LoadObjectV1(pos); + typeCode = (o is string) ? ResourceTypeCode.String : ResourceTypeCode.StartOfUserTypes; + return o; + } + return LoadObjectV2(pos, out typeCode); } - return LoadObjectV2(pos, out typeCode); } // This takes a virtual offset into the data section and reads an Object // from that location. - // Anyone who calls LoadObject should make sure they take a lock so - // no one can cause us to do a seek in here. - internal object? LoadObjectV1(int pos) + private object? LoadObjectV1(int pos) { Debug.Assert(_store != null, "ResourceReader is closed!"); Debug.Assert(_version == 1, ".resources file was not a V1 .resources file!"); + Debug.Assert(Monitor.IsEntered(this)); // uses _store try { @@ -540,6 +554,8 @@ private unsafe string AllocateStringForNameIndex(int index, out int dataOffset) private object? _LoadObjectV1(int pos) { + Debug.Assert(Monitor.IsEntered(this)); // uses _store + _store.BaseStream.Seek(_dataSectionOffset + pos, SeekOrigin.Begin); int typeIndex = _store.Read7BitEncodedInt(); if (typeIndex == -1) @@ -596,10 +612,11 @@ private unsafe string AllocateStringForNameIndex(int index, out int dataOffset) } } - internal object? LoadObjectV2(int pos, out ResourceTypeCode typeCode) + private object? LoadObjectV2(int pos, out ResourceTypeCode typeCode) { Debug.Assert(_store != null, "ResourceReader is closed!"); Debug.Assert(_version >= 2, ".resources file was not a V2 (or higher) .resources file!"); + Debug.Assert(Monitor.IsEntered(this)); // uses _store try { @@ -619,6 +636,8 @@ private unsafe string AllocateStringForNameIndex(int index, out int dataOffset) private object? _LoadObjectV2(int pos, out ResourceTypeCode typeCode) { + Debug.Assert(Monitor.IsEntered(this)); // uses _store + _store.BaseStream.Seek(_dataSectionOffset + pos, SeekOrigin.Begin); typeCode = (ResourceTypeCode)_store.Read7BitEncodedInt(); @@ -755,6 +774,7 @@ private unsafe string AllocateStringForNameIndex(int index, out int dataOffset) [MemberNotNull(nameof(_typeNamePositions))] private void ReadResources() { + Debug.Assert(!Monitor.IsEntered(this)); // only called during init Debug.Assert(_store != null, "ResourceReader is closed!"); try @@ -777,6 +797,8 @@ private void ReadResources() [MemberNotNull(nameof(_typeNamePositions))] private void _ReadResources() { + Debug.Assert(!Monitor.IsEntered(this)); // only called during init + // Read ResourceManager header // Check for magic number int magicNum = _store.ReadInt32(); @@ -962,6 +984,8 @@ private Type FindType(int typeIndex) "Custom readers as well as custom objects on the resources file are not observable by the trimmer and so required assemblies, types and members may be removed.")] private Type UseReflectionToGetType(int typeIndex) { + Debug.Assert(Monitor.IsEntered(this)); // uses _store + long oldPos = _store.BaseStream.Position; try { @@ -1000,6 +1024,8 @@ private Type UseReflectionToGetType(int typeIndex) private string TypeNameFromTypeCode(ResourceTypeCode typeCode) { Debug.Assert(typeCode >= 0, "can't be negative"); + Debug.Assert(Monitor.IsEntered(this)); // uses _store + if (typeCode < ResourceTypeCode.StartOfUserTypes) { Debug.Assert(!string.Equals(typeCode.ToString(), "LastPrimitive"), "Change ResourceTypeCode metadata order so LastPrimitive isn't what Enum.ToString prefers."); @@ -1077,31 +1103,31 @@ public DictionaryEntry Entry if (!_currentIsValid) throw new InvalidOperationException(SR.InvalidOperation_EnumNotStarted); if (_reader._resCache == null) throw new InvalidOperationException(SR.ResourceReaderIsClosed); - string key; + string key = _reader.AllocateStringForNameIndex(_currentName, out _dataPosition); // AllocateStringForNameIndex could lock on _reader + object? value = null; - lock (_reader) - { // locks should be taken in the same order as in RuntimeResourceSet.GetObject to avoid deadlock - lock (_reader._resCache) + // Lock the cache first, then the reader (in this case, we don't actually need to lock the reader and cache at the same time). + // Lock order MUST match RuntimeResourceSet.GetObject to avoid deadlock. + Debug.Assert(!Monitor.IsEntered(_reader)); + lock (_reader._resCache) + { + if (_reader._resCache.TryGetValue(key, out ResourceLocator locator)) { - key = _reader.AllocateStringForNameIndex(_currentName, out _dataPosition); // AllocateStringForNameIndex could lock on _reader - if (_reader._resCache.TryGetValue(key, out ResourceLocator locator)) - { - value = locator.Value; - } - if (value == null) - { - if (_dataPosition == -1) - value = _reader.GetValueForNameIndex(_currentName); - else - value = _reader.LoadObject(_dataPosition); - // If enumeration and subsequent lookups happen very - // frequently in the same process, add a ResourceLocator - // to _resCache here. But WinForms enumerates and - // just about everyone else does lookups. So caching - // here may bloat working set. - } + value = locator.Value; } } + if (value is null) + { + if (_dataPosition == -1) + value = _reader.GetValueForNameIndex(_currentName); + else + value = _reader.LoadObject(_dataPosition); + // If enumeration and subsequent lookups happen very + // frequently in the same process, add a ResourceLocator + // to _resCache here (we'll also need to extend the lock block!). + // But WinForms enumerates and just about everyone else does lookups. + // So caching here may bloat working set. + } return new DictionaryEntry(key, value); } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Resources/RuntimeResourceSet.cs b/src/libraries/System.Private.CoreLib/src/System/Resources/RuntimeResourceSet.cs index 11f3b5eb9515d..957b6269e94db 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Resources/RuntimeResourceSet.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Resources/RuntimeResourceSet.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Threading; namespace System.Resources #if RESOURCES_EXTENSIONS @@ -283,6 +284,9 @@ private IDictionaryEnumerator GetEnumeratorHelper() object? value; ResourceLocator resEntry; + // Lock the cache first, then the reader (reader locks implicitly through its methods). + // Lock order MUST match ResourceReader.ResourceEnumerator.Entry to avoid deadlock. + Debug.Assert(!Monitor.IsEntered(reader)); lock (cache) { // Find the offset within the data section @@ -295,7 +299,7 @@ private IDictionaryEnumerator GetEnumeratorHelper() // When data type cannot be cached dataPos = resEntry.DataPosition; - return isString ? reader.LoadString(dataPos) : reader.LoadObject(dataPos, out _); + return isString ? reader.LoadString(dataPos) : reader.LoadObject(dataPos); } dataPos = reader.FindPosForResource(key); @@ -353,14 +357,11 @@ private IDictionaryEnumerator GetEnumeratorHelper() return value; } - private static object? ReadValue (ResourceReader reader, int dataPos, bool isString, out ResourceLocator locator) + private static object? ReadValue(ResourceReader reader, int dataPos, bool isString, out ResourceLocator locator) { object? value; ResourceTypeCode typeCode; - // Normally calling LoadString or LoadObject requires - // taking a lock. Note that in this case, we took a - // lock before calling this method. if (isString) { value = reader.LoadString(dataPos); diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryResourceWriterUnitTest.cs b/src/libraries/System.Resources.Extensions/tests/BinaryResourceWriterUnitTest.cs index c06921cecc834..21accbca7fe12 100644 --- a/src/libraries/System.Resources.Extensions/tests/BinaryResourceWriterUnitTest.cs +++ b/src/libraries/System.Resources.Extensions/tests/BinaryResourceWriterUnitTest.cs @@ -10,6 +10,8 @@ using System.Linq; using System.Reflection; using System.Runtime.Serialization.Formatters.Binary; +using System.Threading; +using System.Threading.Tasks; using Xunit; namespace System.Resources.Extensions.Tests @@ -151,7 +153,6 @@ public static void EmptyResources() Assert.Equal(writerBuffer, binaryWriterBuffer); } - [Fact] public static void PrimitiveResources() { @@ -500,6 +501,50 @@ public static void EmbeddedResourcesAreUpToDate() } } + /// + /// This test has multiple threads simultaneously loop over the keys of a moderately-sized resx using + /// and call for each key. + /// This has historically been prone to thread-safety bugs because of the shared cache state and internal + /// method calls between RuntimeResourceSet and . + /// + /// Running with TRUE replicates https://github.com/dotnet/runtime/issues/74868, + /// while running with FALSE replicates the error from https://github.com/dotnet/runtime/issues/74052. + /// + /// + /// Whether to use vs. when enumerating; + /// these follow fairly different code paths. + /// ] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsBinaryFormatterSupported))] + [InlineData(false)] + [InlineData(true)] + public static void TestResourceManagerIsSafeForConcurrentAccessAndEnumeration(bool useEnumeratorEntry) + { + ResourceManager manager = new( + typeof(TestData).FullName, + typeof(TestData).Assembly, + typeof(DeserializingResourceReader).Assembly.GetType("System.Resources.Extensions.RuntimeResourceSet", throwOnError: true)); + + const int Threads = 10; + using Barrier barrier = new(Threads); + Task task = Task.WhenAll(Enumerable.Range(0, Threads).Select(_ => Task.Run(WaitForBarrierThenEnumerateResources))); + + Assert.True(task.Wait(TimeSpan.FromSeconds(30))); + + void WaitForBarrierThenEnumerateResources() + { + barrier.SignalAndWait(); + + ResourceSet set = manager.GetResourceSet(CultureInfo.InvariantCulture, createIfNotExists: true, tryParents: true); + IDictionaryEnumerator enumerator = set.GetEnumerator(); + while (enumerator.MoveNext()) + { + object key = useEnumeratorEntry ? enumerator.Entry.Key : enumerator.Key; + manager.GetObject((string)key); + Thread.Sleep(1); + } + } + } + private static void ResourceValueEquals(object expected, object actual) { if (actual is Bitmap bitmap) @@ -537,5 +582,4 @@ private static void BitmapEquals(Bitmap left, Bitmap right) } } } - } diff --git a/src/libraries/System.Resources.ResourceManager/tests/ResourceManagerTests.cs b/src/libraries/System.Resources.ResourceManager/tests/ResourceManagerTests.cs index ebdac1cd0c48a..a9dd8253b60c2 100644 --- a/src/libraries/System.Resources.ResourceManager/tests/ResourceManagerTests.cs +++ b/src/libraries/System.Resources.ResourceManager/tests/ResourceManagerTests.cs @@ -12,6 +12,9 @@ using System.Diagnostics; using Microsoft.DotNet.RemoteExecutor; using Xunit; +using System.Threading; +using System.Threading.Tasks; +using System.Collections; [assembly:NeutralResourcesLanguage("en")] @@ -222,6 +225,52 @@ public static void IgnoreCase(string key, string expectedValue) Assert.Equal(expectedValue, manager.GetString(key.ToLower(), culture)); } + /// + /// This test has multiple threads simultaneously loop over the keys of a moderately-sized resx using + /// and call for each key. + /// This has historically been prone to thread-safety bugs because of the shared cache state and internal + /// method calls between RuntimeResourceSet and . + /// + /// Running with TRUE replicates https://github.com/dotnet/runtime/issues/74868, + /// while running with FALSE replicates the error from https://github.com/dotnet/runtime/issues/74052. + /// + /// + /// Whether to use vs. when enumerating; + /// these follow fairly different code paths. + /// + [Theory] + [InlineData(false)] + [InlineData(true)] + public static void TestResourceManagerIsSafeForConcurrentAccessAndEnumeration(bool useEnumeratorEntry) + { + ResourceManager manager = new("System.Resources.Tests.Resources.AToZResx", typeof(ResourceManagerTests).GetTypeInfo().Assembly); + + const int Threads = 10; + using Barrier barrier = new(Threads); + Task[] tasks = Enumerable.Range(0, Threads) + .Select(_ => Task.Factory.StartNew( + WaitForBarrierThenEnumerateResources, + CancellationToken.None, + TaskCreationOptions.LongRunning, + TaskScheduler.Default)) + .ToArray(); + + Assert.True(Task.WaitAll(tasks, TimeSpan.FromSeconds(30))); + + void WaitForBarrierThenEnumerateResources() + { + barrier.SignalAndWait(); + + ResourceSet set = manager.GetResourceSet(CultureInfo.InvariantCulture, createIfNotExists: true, tryParents: true); + IDictionaryEnumerator enumerator = set.GetEnumerator(); + while (enumerator.MoveNext()) + { + object key = useEnumeratorEntry ? enumerator.Entry.Key : enumerator.Key; + manager.GetObject((string)key); + Thread.Sleep(1); + } + } + } public static IEnumerable EnglishNonStringResourceData() { diff --git a/src/libraries/System.Resources.ResourceManager/tests/Resources/AToZResx.Designer.cs b/src/libraries/System.Resources.ResourceManager/tests/Resources/AToZResx.Designer.cs new file mode 100644 index 0000000000000..047939cdc644a --- /dev/null +++ b/src/libraries/System.Resources.ResourceManager/tests/Resources/AToZResx.Designer.cs @@ -0,0 +1,297 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace System.Resources.Tests.Resources { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class AToZResx { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal AToZResx() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("System.Resources.Tests.Resources.AToZResx", typeof(AToZResx).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to a. + /// + internal static string A { + get { + return ResourceManager.GetString("A", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to b. + /// + internal static string B { + get { + return ResourceManager.GetString("B", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to c. + /// + internal static string C { + get { + return ResourceManager.GetString("C", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to d. + /// + internal static string D { + get { + return ResourceManager.GetString("D", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to e. + /// + internal static string E { + get { + return ResourceManager.GetString("E", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to f. + /// + internal static string F { + get { + return ResourceManager.GetString("F", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to g. + /// + internal static string G { + get { + return ResourceManager.GetString("G", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to h. + /// + internal static string H { + get { + return ResourceManager.GetString("H", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to i. + /// + internal static string I { + get { + return ResourceManager.GetString("I", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to j. + /// + internal static string J { + get { + return ResourceManager.GetString("J", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to k. + /// + internal static string K { + get { + return ResourceManager.GetString("K", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to l. + /// + internal static string L { + get { + return ResourceManager.GetString("L", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to m. + /// + internal static string M { + get { + return ResourceManager.GetString("M", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to n. + /// + internal static string N { + get { + return ResourceManager.GetString("N", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to o. + /// + internal static string O { + get { + return ResourceManager.GetString("O", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to p. + /// + internal static string P { + get { + return ResourceManager.GetString("P", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to q. + /// + internal static string Q { + get { + return ResourceManager.GetString("Q", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to r. + /// + internal static string R { + get { + return ResourceManager.GetString("R", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to s. + /// + internal static string S { + get { + return ResourceManager.GetString("S", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to t. + /// + internal static string T { + get { + return ResourceManager.GetString("T", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to u. + /// + internal static string U { + get { + return ResourceManager.GetString("U", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to v. + /// + internal static string V { + get { + return ResourceManager.GetString("V", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to w. + /// + internal static string W { + get { + return ResourceManager.GetString("W", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to x. + /// + internal static string X { + get { + return ResourceManager.GetString("X", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to y. + /// + internal static string Y { + get { + return ResourceManager.GetString("Y", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to z. + /// + internal static string Z { + get { + return ResourceManager.GetString("Z", resourceCulture); + } + } + } +} diff --git a/src/libraries/System.Resources.ResourceManager/tests/Resources/AToZResx.resx b/src/libraries/System.Resources.ResourceManager/tests/Resources/AToZResx.resx new file mode 100644 index 0000000000000..e1c5ddaba7ed8 --- /dev/null +++ b/src/libraries/System.Resources.ResourceManager/tests/Resources/AToZResx.resx @@ -0,0 +1,198 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + a + + + b + + + c + + + d + + + e + + + f + + + g + + + h + + + i + + + j + + + k + + + l + + + m + + + n + + + o + + + p + + + q + + + r + + + s + + + t + + + u + + + v + + + w + + + x + + + y + + + z + + \ No newline at end of file diff --git a/src/libraries/System.Resources.ResourceManager/tests/System.Resources.ResourceManager.Tests.csproj b/src/libraries/System.Resources.ResourceManager/tests/System.Resources.ResourceManager.Tests.csproj index 66cb937a3d1e2..f5ee7f0c22b59 100644 --- a/src/libraries/System.Resources.ResourceManager/tests/System.Resources.ResourceManager.Tests.csproj +++ b/src/libraries/System.Resources.ResourceManager/tests/System.Resources.ResourceManager.Tests.csproj @@ -8,6 +8,11 @@ + + True + True + AToZResx.resx + @@ -19,10 +24,13 @@ - + + + ResXFileCodeGenerator + AToZResx.Designer.cs + false Non-Resx @@ -40,23 +48,14 @@ ResXFileCodeGenerator TestResx.Designer.cs - - - - + + + + - - - + + +