Cache Invariant CompareInfo (#15902)
[platform/upstream/coreclr.git] / src / mscorlib / src / System / Resources / ManifestBasedResourceGroveler.cs
1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3 // See the LICENSE file in the project root for more information.
4
5 /*============================================================
6 **
7 ** 
8 ** 
9 **
10 **
11 ** Purpose: Searches for resources in Assembly manifest, used
12 ** for assembly-based resource lookup.
13 **
14 ** 
15 ===========================================================*/
16
17 namespace System.Resources
18 {
19     using System;
20     using System.Collections;
21     using System.Collections.Generic;
22     using System.Globalization;
23     using System.IO;
24     using System.Reflection;
25     using System.Runtime.InteropServices;
26     using System.Runtime.CompilerServices;
27     using System.Runtime.Versioning;
28     using System.Text;
29     using System.Threading;
30     using System.Diagnostics;
31     using Microsoft.Win32;
32
33     //
34     // Note: this type is integral to the construction of exception objects,
35     // and sometimes this has to be done in low memory situtations (OOM) or
36     // to create TypeInitializationExceptions due to failure of a static class
37     // constructor. This type needs to be extremely careful and assume that 
38     // any type it references may have previously failed to construct, so statics
39     // belonging to that type may not be initialized. FrameworkEventSource.Log
40     // is one such example.
41     //
42     internal class ManifestBasedResourceGroveler : IResourceGroveler
43     {
44         private ResourceManager.ResourceManagerMediator _mediator;
45
46         public ManifestBasedResourceGroveler(ResourceManager.ResourceManagerMediator mediator)
47         {
48             // here and below: convert asserts to preconditions where appropriate when we get
49             // contracts story in place.
50             Debug.Assert(mediator != null, "mediator shouldn't be null; check caller");
51             _mediator = mediator;
52         }
53
54         public ResourceSet GrovelForResourceSet(CultureInfo culture, Dictionary<String, ResourceSet> localResourceSets, bool tryParents, bool createIfNotExists, ref StackCrawlMark stackMark)
55         {
56             Debug.Assert(culture != null, "culture shouldn't be null; check caller");
57             Debug.Assert(localResourceSets != null, "localResourceSets shouldn't be null; check caller");
58
59             ResourceSet rs = null;
60             Stream stream = null;
61             RuntimeAssembly satellite = null;
62
63             // 1. Fixups for ultimate fallbacks
64             CultureInfo lookForCulture = UltimateFallbackFixup(culture);
65
66             // 2. Look for satellite assembly or main assembly, as appropriate
67             if (lookForCulture.HasInvariantCultureName && _mediator.FallbackLoc == UltimateResourceFallbackLocation.MainAssembly)
68             {
69                 // don't bother looking in satellites in this case
70                 satellite = _mediator.MainAssembly;
71             }
72 #if RESOURCE_SATELLITE_CONFIG
73             // If our config file says the satellite isn't here, don't ask for it.
74             else if (!lookForCulture.HasInvariantCultureName && !_mediator.TryLookingForSatellite(lookForCulture))
75             {
76                 satellite = null;
77             }
78 #endif
79             else
80             {
81                 satellite = GetSatelliteAssembly(lookForCulture, ref stackMark);
82
83                 if (satellite == null)
84                 {
85                     bool raiseException = (culture.HasInvariantCultureName && (_mediator.FallbackLoc == UltimateResourceFallbackLocation.Satellite));
86                     // didn't find satellite, give error if necessary
87                     if (raiseException)
88                     {
89                         HandleSatelliteMissing();
90                     }
91                 }
92             }
93
94             // get resource file name we'll search for. Note, be careful if you're moving this statement
95             // around because lookForCulture may be modified from originally requested culture above.
96             String fileName = _mediator.GetResourceFileName(lookForCulture);
97
98             // 3. If we identified an assembly to search; look in manifest resource stream for resource file
99             if (satellite != null)
100             {
101                 // Handle case in here where someone added a callback for assembly load events.
102                 // While no other threads have called into GetResourceSet, our own thread can!
103                 // At that point, we could already have an RS in our hash table, and we don't 
104                 // want to add it twice.
105                 lock (localResourceSets)
106                 {
107                     localResourceSets.TryGetValue(culture.Name, out rs);
108                 }
109
110                 stream = GetManifestResourceStream(satellite, fileName, ref stackMark);
111             }
112
113             // 4a. Found a stream; create a ResourceSet if possible
114             if (createIfNotExists && stream != null && rs == null)
115             {
116                 rs = CreateResourceSet(stream, satellite);
117             }
118             else if (stream == null && tryParents)
119             {
120                 // 4b. Didn't find stream; give error if necessary
121                 bool raiseException = culture.HasInvariantCultureName;
122                 if (raiseException)
123                 {
124                     HandleResourceStreamMissing(fileName);
125                 }
126             }
127
128             return rs;
129         }
130
131         private CultureInfo UltimateFallbackFixup(CultureInfo lookForCulture)
132         {
133             CultureInfo returnCulture = lookForCulture;
134
135             // If our neutral resources were written in this culture AND we know the main assembly
136             // does NOT contain neutral resources, don't probe for this satellite.
137             if (lookForCulture.Name == _mediator.NeutralResourcesCulture.Name &&
138                 _mediator.FallbackLoc == UltimateResourceFallbackLocation.MainAssembly)
139             {
140                 returnCulture = CultureInfo.InvariantCulture;
141             }
142             else if (lookForCulture.HasInvariantCultureName && _mediator.FallbackLoc == UltimateResourceFallbackLocation.Satellite)
143             {
144                 returnCulture = _mediator.NeutralResourcesCulture;
145             }
146
147             return returnCulture;
148         }
149
150         internal static CultureInfo GetNeutralResourcesLanguage(Assembly a, ref UltimateResourceFallbackLocation fallbackLocation)
151         {
152             Debug.Assert(a != null, "assembly != null");
153             string cultureName = null;
154             short fallback = 0;
155             if (GetNeutralResourcesLanguageAttribute(((RuntimeAssembly)a).GetNativeHandle(),
156                                                         JitHelpers.GetStringHandleOnStack(ref cultureName),
157                                                         out fallback))
158             {
159                 if ((UltimateResourceFallbackLocation)fallback < UltimateResourceFallbackLocation.MainAssembly || (UltimateResourceFallbackLocation)fallback > UltimateResourceFallbackLocation.Satellite)
160                 {
161                     throw new ArgumentException(SR.Format(SR.Arg_InvalidNeutralResourcesLanguage_FallbackLoc, fallback));
162                 }
163                 fallbackLocation = (UltimateResourceFallbackLocation)fallback;
164             }
165             else
166             {
167                 fallbackLocation = UltimateResourceFallbackLocation.MainAssembly;
168                 return CultureInfo.InvariantCulture;
169             }
170
171             try
172             {
173                 CultureInfo c = CultureInfo.GetCultureInfo(cultureName);
174                 return c;
175             }
176             catch (ArgumentException e)
177             { // we should catch ArgumentException only.
178                 // Note we could go into infinite loops if mscorlib's 
179                 // NeutralResourcesLanguageAttribute is mangled.  If this assert
180                 // fires, please fix the build process for the BCL directory.
181                 if (a == typeof(Object).Assembly)
182                 {
183                     Debug.Fail(System.CoreLib.Name + "'s NeutralResourcesLanguageAttribute is a malformed culture name! name: \"" + cultureName + "\"  Exception: " + e);
184                     return CultureInfo.InvariantCulture;
185                 }
186
187                 throw new ArgumentException(SR.Format(SR.Arg_InvalidNeutralResourcesLanguage_Asm_Culture, a.ToString(), cultureName), e);
188             }
189         }
190
191         // Constructs a new ResourceSet for a given file name.
192         // Use the assembly to resolve assembly manifest resource references.
193         // Note that is can be null, but probably shouldn't be.
194         // This method could use some refactoring. One thing at a time.
195         internal ResourceSet CreateResourceSet(Stream store, Assembly assembly)
196         {
197             Debug.Assert(store != null, "I need a Stream!");
198             // Check to see if this is a Stream the ResourceManager understands,
199             // and check for the correct resource reader type.
200             if (store.CanSeek && store.Length > 4)
201             {
202                 long startPos = store.Position;
203
204                 // not disposing because we want to leave stream open
205                 BinaryReader br = new BinaryReader(store);
206
207                 // Look for our magic number as a little endian Int32.
208                 int bytes = br.ReadInt32();
209                 if (bytes == ResourceManager.MagicNumber)
210                 {
211                     int resMgrHeaderVersion = br.ReadInt32();
212                     String readerTypeName = null, resSetTypeName = null;
213                     if (resMgrHeaderVersion == ResourceManager.HeaderVersionNumber)
214                     {
215                         br.ReadInt32();  // We don't want the number of bytes to skip.
216                         readerTypeName = System.CoreLib.FixupCoreLibName(br.ReadString());
217                         resSetTypeName = System.CoreLib.FixupCoreLibName(br.ReadString());
218                     }
219                     else if (resMgrHeaderVersion > ResourceManager.HeaderVersionNumber)
220                     {
221                         // Assume that the future ResourceManager headers will
222                         // have two strings for us - the reader type name and
223                         // resource set type name.  Read those, then use the num
224                         // bytes to skip field to correct our position.
225                         int numBytesToSkip = br.ReadInt32();
226                         long endPosition = br.BaseStream.Position + numBytesToSkip;
227
228                         readerTypeName = System.CoreLib.FixupCoreLibName(br.ReadString());
229                         resSetTypeName = System.CoreLib.FixupCoreLibName(br.ReadString());
230
231                         br.BaseStream.Seek(endPosition, SeekOrigin.Begin);
232                     }
233                     else
234                     {
235                         // resMgrHeaderVersion is older than this ResMgr version.
236                         // We should add in backwards compatibility support here.
237
238                         throw new NotSupportedException(SR.Format(SR.NotSupported_ObsoleteResourcesFile, _mediator.MainAssembly.GetSimpleName()));
239                     }
240
241                     store.Position = startPos;
242                     // Perf optimization - Don't use Reflection for our defaults.
243                     // Note there are two different sets of strings here - the
244                     // assembly qualified strings emitted by ResourceWriter, and
245                     // the abbreviated ones emitted by InternalResGen.
246                     if (CanUseDefaultResourceClasses(readerTypeName, resSetTypeName))
247                     {
248                         return new RuntimeResourceSet(store);
249                     }
250                     else
251                     {
252                         // we do not want to use partial binding here.
253                         Type readerType = Type.GetType(readerTypeName, true);
254                         Object[] args = new Object[1];
255                         args[0] = store;
256                         IResourceReader reader = (IResourceReader)Activator.CreateInstance(readerType, args);
257
258                         Object[] resourceSetArgs = new Object[1];
259                         resourceSetArgs[0] = reader;
260
261                         Type resSetType;
262                         if (_mediator.UserResourceSet == null)
263                         {
264                             Debug.Assert(resSetTypeName != null, "We should have a ResourceSet type name from the custom resource file here.");
265                             resSetType = Type.GetType(resSetTypeName, true, false);
266                         }
267                         else
268                             resSetType = _mediator.UserResourceSet;
269                         ResourceSet rs = (ResourceSet)Activator.CreateInstance(resSetType,
270                                                                                 BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.CreateInstance,
271                                                                                 null,
272                                                                                 resourceSetArgs,
273                                                                                 null,
274                                                                                 null);
275                         return rs;
276                     }
277                 }
278                 else
279                 {
280                     store.Position = startPos;
281                 }
282             }
283
284             if (_mediator.UserResourceSet == null)
285             {
286                 return new RuntimeResourceSet(store);
287             }
288             else
289             {
290                 Object[] args = new Object[2];
291                 args[0] = store;
292                 args[1] = assembly;
293                 try
294                 {
295                     ResourceSet rs = null;
296                     // Add in a check for a constructor taking in an assembly first.
297                     try
298                     {
299                         rs = (ResourceSet)Activator.CreateInstance(_mediator.UserResourceSet, args);
300                         return rs;
301                     }
302                     catch (MissingMethodException) { }
303
304                     args = new Object[1];
305                     args[0] = store;
306                     rs = (ResourceSet)Activator.CreateInstance(_mediator.UserResourceSet, args);
307
308                     return rs;
309                 }
310                 catch (MissingMethodException e)
311                 {
312                     throw new InvalidOperationException(SR.Format(SR.InvalidOperation_ResMgrBadResSet_Type, _mediator.UserResourceSet.AssemblyQualifiedName), e);
313                 }
314             }
315         }
316
317         private Stream GetManifestResourceStream(RuntimeAssembly satellite, String fileName, ref StackCrawlMark stackMark)
318         {
319             Debug.Assert(satellite != null, "satellite shouldn't be null; check caller");
320             Debug.Assert(fileName != null, "fileName shouldn't be null; check caller");
321
322             // If we're looking in the main assembly AND if the main assembly was the person who
323             // created the ResourceManager, skip a security check for private manifest resources.
324             bool canSkipSecurityCheck = (_mediator.MainAssembly == satellite)
325                                         && (_mediator.CallingAssembly == _mediator.MainAssembly);
326
327             Stream stream = satellite.GetManifestResourceStream(_mediator.LocationInfo, fileName, canSkipSecurityCheck, ref stackMark);
328             if (stream == null)
329             {
330                 stream = CaseInsensitiveManifestResourceStreamLookup(satellite, fileName);
331             }
332
333             return stream;
334         }
335
336         // Looks up a .resources file in the assembly manifest using 
337         // case-insensitive lookup rules.  Yes, this is slow.  The metadata
338         // dev lead refuses to make all assembly manifest resource lookups case-insensitive,
339         // even optionally case-insensitive.        
340         [System.Security.DynamicSecurityMethod] // Methods containing StackCrawlMark local var has to be marked DynamicSecurityMethod
341         private Stream CaseInsensitiveManifestResourceStreamLookup(RuntimeAssembly satellite, String name)
342         {
343             Debug.Assert(satellite != null, "satellite shouldn't be null; check caller");
344             Debug.Assert(name != null, "name shouldn't be null; check caller");
345
346             StringBuilder sb = new StringBuilder();
347             if (_mediator.LocationInfo != null)
348             {
349                 String nameSpace = _mediator.LocationInfo.Namespace;
350                 if (nameSpace != null)
351                 {
352                     sb.Append(nameSpace);
353                     if (name != null)
354                         sb.Append(Type.Delimiter);
355                 }
356             }
357             sb.Append(name);
358
359             String givenName = sb.ToString();
360             String canonicalName = null;
361             foreach (String existingName in satellite.GetManifestResourceNames())
362             {
363                 if (String.Equals(existingName, givenName, StringComparison.InvariantCultureIgnoreCase))
364                 {
365                     if (canonicalName == null)
366                     {
367                         canonicalName = existingName;
368                     }
369                     else
370                     {
371                         throw new MissingManifestResourceException(SR.Format(SR.MissingManifestResource_MultipleBlobs, givenName, satellite.ToString()));
372                     }
373                 }
374             }
375
376             if (canonicalName == null)
377             {
378                 return null;
379             }
380
381             // If we're looking in the main assembly AND if the main
382             // assembly was the person who created the ResourceManager,
383             // skip a security check for private manifest resources.
384             bool canSkipSecurityCheck = _mediator.MainAssembly == satellite && _mediator.CallingAssembly == _mediator.MainAssembly;
385             StackCrawlMark stackMark = StackCrawlMark.LookForMyCaller;
386             return satellite.GetManifestResourceStream(canonicalName, ref stackMark, canSkipSecurityCheck);
387         }
388
389         private RuntimeAssembly GetSatelliteAssembly(CultureInfo lookForCulture, ref StackCrawlMark stackMark)
390         {
391             if (!_mediator.LookedForSatelliteContractVersion)
392             {
393                 _mediator.SatelliteContractVersion = _mediator.ObtainSatelliteContractVersion(_mediator.MainAssembly);
394                 _mediator.LookedForSatelliteContractVersion = true;
395             }
396
397             RuntimeAssembly satellite = null;
398             String satAssemblyName = GetSatelliteAssemblyName();
399
400             // Look up the satellite assembly, but don't let problems
401             // like a partially signed satellite assembly stop us from
402             // doing fallback and displaying something to the user.
403             // Yet also somehow log this error for a developer.
404             try
405             {
406                 satellite = _mediator.MainAssembly.InternalGetSatelliteAssembly(satAssemblyName, lookForCulture, _mediator.SatelliteContractVersion, false, ref stackMark);
407             }
408
409             // Jun 08: for cases other than ACCESS_DENIED, we'll assert instead of throw to give release builds more opportunity to fallback.
410
411             catch (FileLoadException fle)
412             {
413                 // Ignore cases where the loader gets an access
414                 // denied back from the OS.  This showed up for
415                 // href-run exe's at one point.  
416                 int hr = fle._HResult;
417                 if (hr != Win32Marshal.MakeHRFromErrorCode(Win32Native.ERROR_ACCESS_DENIED))
418                 {
419                     Debug.Fail("[This assert catches satellite assembly build/deployment problems - report this message to your build lab & loc engineer]" + Environment.NewLine + "GetSatelliteAssembly failed for culture " + lookForCulture.Name + " and version " + (_mediator.SatelliteContractVersion == null ? _mediator.MainAssembly.GetVersion().ToString() : _mediator.SatelliteContractVersion.ToString()) + " of assembly " + _mediator.MainAssembly.GetSimpleName() + " with error code 0x" + hr.ToString("X", CultureInfo.InvariantCulture) + Environment.NewLine + "Exception: " + fle);
420                 }
421             }
422
423             // Don't throw for zero-length satellite assemblies, for compat with v1
424             catch (BadImageFormatException bife)
425             {
426                 Debug.Fail("[This assert catches satellite assembly build/deployment problems - report this message to your build lab & loc engineer]" + Environment.NewLine + "GetSatelliteAssembly failed for culture " + lookForCulture.Name + " and version " + (_mediator.SatelliteContractVersion == null ? _mediator.MainAssembly.GetVersion().ToString() : _mediator.SatelliteContractVersion.ToString()) + " of assembly " + _mediator.MainAssembly.GetSimpleName() + Environment.NewLine + "Exception: " + bife);
427             }
428
429             return satellite;
430         }
431
432         // Perf optimization - Don't use Reflection for most cases with
433         // our .resources files.  This makes our code run faster and we can
434         // creating a ResourceReader via Reflection.  This would incur
435         // a security check (since the link-time check on the constructor that
436         // takes a String is turned into a full demand with a stack walk)
437         // and causes partially trusted localized apps to fail.
438         private bool CanUseDefaultResourceClasses(String readerTypeName, String resSetTypeName)
439         {
440             Debug.Assert(readerTypeName != null, "readerTypeName shouldn't be null; check caller");
441             Debug.Assert(resSetTypeName != null, "resSetTypeName shouldn't be null; check caller");
442
443             if (_mediator.UserResourceSet != null)
444                 return false;
445
446             // Ignore the actual version of the ResourceReader and 
447             // RuntimeResourceSet classes.  Let those classes deal with
448             // versioning themselves.
449             AssemblyName mscorlib = new AssemblyName(ResourceManager.MscorlibName);
450
451             if (readerTypeName != null)
452             {
453                 if (!ResourceManager.CompareNames(readerTypeName, ResourceManager.ResReaderTypeName, mscorlib))
454                     return false;
455             }
456
457             if (resSetTypeName != null)
458             {
459                 if (!ResourceManager.CompareNames(resSetTypeName, ResourceManager.ResSetTypeName, mscorlib))
460                     return false;
461             }
462
463             return true;
464         }
465
466         private String GetSatelliteAssemblyName()
467         {
468             String satAssemblyName = _mediator.MainAssembly.GetSimpleName();
469             satAssemblyName += ".resources";
470             return satAssemblyName;
471         }
472
473         private void HandleSatelliteMissing()
474         {
475             String satAssemName = _mediator.MainAssembly.GetSimpleName() + ".resources.dll";
476             if (_mediator.SatelliteContractVersion != null)
477             {
478                 satAssemName += ", Version=" + _mediator.SatelliteContractVersion.ToString();
479             }
480
481             AssemblyName an = new AssemblyName();
482             an.SetPublicKey(_mediator.MainAssembly.GetPublicKey());
483             byte[] token = an.GetPublicKeyToken();
484
485             int iLen = token.Length;
486             StringBuilder publicKeyTok = new StringBuilder(iLen * 2);
487             for (int i = 0; i < iLen; i++)
488             {
489                 publicKeyTok.Append(token[i].ToString("x", CultureInfo.InvariantCulture));
490             }
491             satAssemName += ", PublicKeyToken=" + publicKeyTok;
492
493             String missingCultureName = _mediator.NeutralResourcesCulture.Name;
494             if (missingCultureName.Length == 0)
495             {
496                 missingCultureName = "<invariant>";
497             }
498             throw new MissingSatelliteAssemblyException(SR.Format(SR.MissingSatelliteAssembly_Culture_Name, _mediator.NeutralResourcesCulture, satAssemName), missingCultureName);
499         }
500
501         private void HandleResourceStreamMissing(String fileName)
502         {
503             // Keep people from bothering me about resources problems
504             if (_mediator.MainAssembly == typeof(Object).Assembly && _mediator.BaseName.Equals(System.CoreLib.Name))
505             {
506                 // This would break CultureInfo & all our exceptions.
507                 Debug.Fail("Couldn't get " + System.CoreLib.Name + ResourceManager.ResFileExtension + " from " + System.CoreLib.Name + "'s assembly" + Environment.NewLine + Environment.NewLine + "Are you building the runtime on your machine?  Chances are the BCL directory didn't build correctly.  Type 'build -c' in the BCL directory.  If you get build errors, look at buildd.log.  If you then can't figure out what's wrong (and you aren't changing the assembly-related metadata code), ask a BCL dev.\n\nIf you did NOT build the runtime, you shouldn't be seeing this and you've found a bug.");
508
509                 // We cannot continue further - simply FailFast.
510                 string mesgFailFast = System.CoreLib.Name + ResourceManager.ResFileExtension + " couldn't be found!  Large parts of the BCL won't work!";
511                 System.Environment.FailFast(mesgFailFast);
512             }
513             // We really don't think this should happen - we always
514             // expect the neutral locale's resources to be present.
515             String resName = String.Empty;
516             if (_mediator.LocationInfo != null && _mediator.LocationInfo.Namespace != null)
517                 resName = _mediator.LocationInfo.Namespace + Type.Delimiter;
518             resName += fileName;
519             throw new MissingManifestResourceException(SR.Format(SR.MissingManifestResource_NoNeutralAsm, resName, _mediator.MainAssembly.GetSimpleName()));
520         }
521
522         [DllImport(JitHelpers.QCall, CharSet = CharSet.Unicode)]
523         [return: MarshalAs(UnmanagedType.Bool)]
524         internal static extern bool GetNeutralResourcesLanguageAttribute(RuntimeAssembly assemblyHandle, StringHandleOnStack cultureName, out short fallbackLocation);
525     }
526 }