File: src\libraries\System.Private.CoreLib\src\System\Resources\ManifestBasedResourceGroveler.cs
Web Access
Project: src\src\coreclr\System.Private.CoreLib\System.Private.CoreLib.csproj (System.Private.CoreLib)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Reflection;
 
namespace System.Resources
{
    /// <summary>
    /// Searches for resources in  the assembly manifest, used for assembly-based resource lookup.
    /// </summary>
    // Note: this type is integral to the construction of exception objects,
    // and sometimes this has to be done in low memory situations (OOM) or
    // to create TypeInitializationExceptions due to failure of a static class
    // constructor. This type needs to be extremely careful and assume that
    // any type it references may have previously failed to construct, so statics
    // belonging to that type may not be initialized. FrameworkEventSource.Log
    // is one such example.
    internal sealed partial class ManifestBasedResourceGroveler : IResourceGroveler
    {
        private readonly ResourceManager.ResourceManagerMediator _mediator;
 
        public ManifestBasedResourceGroveler(ResourceManager.ResourceManagerMediator mediator)
        {
            // here and below: convert asserts to preconditions where appropriate when we get
            // contracts story in place.
            Debug.Assert(mediator != null, "mediator shouldn't be null; check caller");
            _mediator = mediator;
        }
 
        public ResourceSet? GrovelForResourceSet(CultureInfo culture, Dictionary<string, ResourceSet> localResourceSets, bool tryParents, bool createIfNotExists)
        {
            Debug.Assert(culture != null, "culture shouldn't be null; check caller");
            Debug.Assert(localResourceSets != null, "localResourceSets shouldn't be null; check caller");
 
            ResourceSet? rs = null;
            Stream? stream = null;
            Assembly? satellite;
 
            // 1. Fixups for ultimate fallbacks
            CultureInfo lookForCulture = UltimateFallbackFixup(culture);
 
            // 2. Look for satellite assembly or main assembly, as appropriate
            if (lookForCulture.HasInvariantCultureName && _mediator.FallbackLoc == UltimateResourceFallbackLocation.MainAssembly)
            {
                // don't bother looking in satellites in this case
                satellite = _mediator.MainAssembly;
            }
            else
            {
                satellite = GetSatelliteAssembly(lookForCulture);
 
                if (satellite == null)
                {
                    bool raiseException = (culture.HasInvariantCultureName && (_mediator.FallbackLoc == UltimateResourceFallbackLocation.Satellite));
                    // didn't find satellite, give error if necessary
                    if (raiseException)
                    {
                        HandleSatelliteMissing();
                    }
                }
            }
 
            // get resource file name we'll search for. Note, be careful if you're moving this statement
            // around because lookForCulture may be modified from originally requested culture above.
            string fileName = _mediator.GetResourceFileName(lookForCulture);
 
            // 3. If we identified an assembly to search; look in manifest resource stream for resource file
            if (satellite != null)
            {
                // Handle case in here where someone added a callback for assembly load events.
                // While no other threads have called into GetResourceSet, our own thread can!
                // At that point, we could already have an RS in our hash table, and we don't
                // want to add it twice.
                lock (localResourceSets)
                {
                    localResourceSets.TryGetValue(culture.Name, out rs);
                }
 
                stream = GetManifestResourceStream(satellite, fileName);
            }
 
            // 4a. Found a stream; create a ResourceSet if possible
            if (createIfNotExists && stream != null && rs == null)
            {
                Debug.Assert(satellite != null, "satellite should not be null when stream is set");
                rs = CreateResourceSet(stream, satellite);
            }
            else if (stream == null && tryParents)
            {
                // 4b. Didn't find stream; give error if necessary
                bool raiseException = culture.HasInvariantCultureName;
                if (raiseException)
                {
                    HandleResourceStreamMissing(fileName);
                }
            }
 
            return rs;
        }
 
        private CultureInfo UltimateFallbackFixup(CultureInfo lookForCulture)
        {
            CultureInfo returnCulture = lookForCulture;
 
            // If our neutral resources were written in this culture AND we know the main assembly
            // does NOT contain neutral resources, don't probe for this satellite.
            Debug.Assert(_mediator.NeutralResourcesCulture != null);
            if (lookForCulture.Name == _mediator.NeutralResourcesCulture.Name &&
                _mediator.FallbackLoc == UltimateResourceFallbackLocation.MainAssembly)
            {
                returnCulture = CultureInfo.InvariantCulture;
            }
            else if (lookForCulture.HasInvariantCultureName && _mediator.FallbackLoc == UltimateResourceFallbackLocation.Satellite)
            {
                returnCulture = _mediator.NeutralResourcesCulture;
            }
 
            return returnCulture;
        }
 
        internal static CultureInfo GetNeutralResourcesLanguage(Assembly a, out UltimateResourceFallbackLocation fallbackLocation)
        {
            Debug.Assert(a != null, "assembly != null");
 
            NeutralResourcesLanguageAttribute? attr = a.GetCustomAttribute<NeutralResourcesLanguageAttribute>();
            if (attr == null || (GlobalizationMode.Invariant && GlobalizationMode.PredefinedCulturesOnly))
            {
                fallbackLocation = UltimateResourceFallbackLocation.MainAssembly;
                return CultureInfo.InvariantCulture;
            }
 
            fallbackLocation = attr.Location;
            if (fallbackLocation < UltimateResourceFallbackLocation.MainAssembly || fallbackLocation > UltimateResourceFallbackLocation.Satellite)
            {
                throw new ArgumentException(SR.Format(SR.Arg_InvalidNeutralResourcesLanguage_FallbackLoc, fallbackLocation));
            }
 
            try
            {
                return CultureInfo.GetCultureInfo(attr.CultureName);
            }
            catch (ArgumentException e)
            { // we should catch ArgumentException only.
                // Note we could go into infinite loops if mscorlib's
                // NeutralResourcesLanguageAttribute is mangled.  If this assert
                // fires, please fix the build process for the BCL directory.
                if (a == typeof(object).Assembly)
                {
                    Debug.Fail(CoreLib.Name + "'s NeutralResourcesLanguageAttribute is a malformed culture name! name: \"" + attr.CultureName + "\"  Exception: " + e);
                    return CultureInfo.InvariantCulture;
                }
 
                throw new ArgumentException(SR.Format(SR.Arg_InvalidNeutralResourcesLanguage_Asm_Culture, a, attr.CultureName), e);
            }
        }
 
        // Constructs a new ResourceSet for a given file name.
        // Use the assembly to resolve assembly manifest resource references.
        // Note that is can be null, but probably shouldn't be.
        // This method could use some refactoring. One thing at a time.
        internal ResourceSet CreateResourceSet(Stream store, Assembly assembly)
        {
            Debug.Assert(store != null, "I need a Stream!");
            // Check to see if this is a Stream the ResourceManager understands,
            // and check for the correct resource reader type.
            if (store.CanSeek && store.Length > 4)
            {
                long startPos = store.Position;
 
                // not disposing because we want to leave stream open
                BinaryReader br = new BinaryReader(store);
 
                // Look for our magic number as a little endian int.
                int bytes = br.ReadInt32();
                if (bytes == ResourceManager.MagicNumber)
                {
                    int resMgrHeaderVersion = br.ReadInt32();
                    string? readerTypeName, resSetTypeName;
                    if (resMgrHeaderVersion == ResourceManager.HeaderVersionNumber)
                    {
                        br.ReadInt32();  // We don't want the number of bytes to skip.
                        readerTypeName = br.ReadString();
                        resSetTypeName = br.ReadString();
                    }
                    else if (resMgrHeaderVersion > ResourceManager.HeaderVersionNumber)
                    {
                        // Assume that the future ResourceManager headers will
                        // have two strings for us - the reader type name and
                        // resource set type name.  Read those, then use the num
                        // bytes to skip field to correct our position.
                        int numBytesToSkip = br.ReadInt32();
                        long endPosition = br.BaseStream.Position + numBytesToSkip;
 
                        readerTypeName = br.ReadString();
                        resSetTypeName = br.ReadString();
 
                        br.BaseStream.Seek(endPosition, SeekOrigin.Begin);
                    }
                    else
                    {
                        // resMgrHeaderVersion is older than this ResMgr version.
                        // We should add in backwards compatibility support here.
                        Debug.Assert(_mediator.MainAssembly != null);
                        throw new NotSupportedException(SR.Format(SR.NotSupported_ObsoleteResourcesFile, _mediator.MainAssembly.GetName().Name));
                    }
 
                    store.Position = startPos;
                    // Perf optimization - Don't use Reflection for our defaults.
                    // Note there are two different sets of strings here - the
                    // assembly qualified strings emitted by ResourceWriter, and
                    // the abbreviated ones emitted by InternalResGen.
                    if (CanUseDefaultResourceClasses(readerTypeName, resSetTypeName))
                    {
                        return new RuntimeResourceSet(store, permitDeserialization: true);
                    }
                    else
                    {
                        if (ResourceReader.AllowCustomResourceTypes)
                        {
                            Debug.Assert(readerTypeName != null, "Reader Type name should be set");
                            Debug.Assert(resSetTypeName != null, "ResourceSet Type name should be set");
#pragma warning disable IL2026 // suppressed in ILLink.Suppressions.LibraryBuild.xml
                            return InternalGetResourceSetFromSerializedData(store, readerTypeName, resSetTypeName, _mediator);
#pragma warning restore IL2026
                        }
                        else
                        {
                            throw new NotSupportedException(SR.ResourceManager_ReflectionNotAllowed);
                        }
                    }
                }
                else
                {
                    store.Position = startPos;
                }
            }
 
            if (_mediator.UserResourceSet == null)
            {
                return new RuntimeResourceSet(store, permitDeserialization: true);
            }
            else
            {
                object[] args = [store, assembly];
                try
                {
                    // Add in a check for a constructor taking in an assembly first.
                    try
                    {
                        return (ResourceSet)Activator.CreateInstance(_mediator.UserResourceSet, args)!;
                    }
                    catch (MissingMethodException) { }
 
                    args = new object[1];
                    args[0] = store;
                    return (ResourceSet)Activator.CreateInstance(_mediator.UserResourceSet, args)!;
                }
                catch (MissingMethodException e)
                {
                    throw new InvalidOperationException(SR.Format(SR.InvalidOperation_ResMgrBadResSet_Type, _mediator.UserResourceSet.AssemblyQualifiedName), e);
                }
            }
        }
 
        private static Assembly? InternalGetSatelliteAssembly(Assembly mainAssembly, CultureInfo culture, Version? version)
        {
            return RuntimeAssembly.InternalGetSatelliteAssembly(mainAssembly, culture, version, throwOnFileNotFound: false);
        }
 
        [RequiresUnreferencedCode("The CustomResourceTypesSupport feature switch has been enabled for this app which is being trimmed. " +
            "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 static ResourceSet InternalGetResourceSetFromSerializedData(Stream store, string readerTypeName, string? resSetTypeName, ResourceManager.ResourceManagerMediator mediator)
        {
            IResourceReader reader;
 
            // Permit deserialization as long as the default ResourceReader is used
            if (ResourceManager.IsDefaultType(readerTypeName, ResourceManager.ResReaderTypeName))
            {
                reader = new ResourceReader(
                    store,
                    new Dictionary<string, ResourceLocator>(FastResourceComparer.Default),
                    permitDeserialization: true);
            }
            else
            {
                Type readerType = Type.GetType(readerTypeName, throwOnError: true)!;
                object[] args = [store];
                reader = (IResourceReader)Activator.CreateInstance(readerType, args)!;
            }
 
            object[] resourceSetArgs = [reader];
            Type? resSetType = mediator.UserResourceSet;
            if (resSetType == null)
            {
                Debug.Assert(resSetTypeName != null, "We should have a ResourceSet type name from the custom resource file here.");
                resSetType = Type.GetType(resSetTypeName, true, false)!;
            }
 
            ResourceSet rs = (ResourceSet)Activator.CreateInstance(resSetType,
                                                                    BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.CreateInstance,
                                                                    null,
                                                                    resourceSetArgs,
                                                                    null,
                                                                    null)!;
            return rs;
        }
 
        private static Stream? GetManifestResourceStream(Assembly satellite, string fileName)
        {
            Debug.Assert(satellite != null, "satellite shouldn't be null; check caller");
            Debug.Assert(fileName != null, "fileName shouldn't be null; check caller");
 
            return satellite.GetManifestResourceStream(fileName) ??
                CaseInsensitiveManifestResourceStreamLookup(satellite, fileName);
        }
 
        // Looks up a .resources file in the assembly manifest using
        // case-insensitive lookup rules.  Yes, this is slow.  The metadata
        // dev lead refuses to make all assembly manifest resource lookups case-insensitive,
        // even optionally case-insensitive.
        private static Stream? CaseInsensitiveManifestResourceStreamLookup(Assembly satellite, string name)
        {
            Debug.Assert(satellite != null, "satellite shouldn't be null; check caller");
            Debug.Assert(name != null, "name shouldn't be null; check caller");
 
            string? canonicalName = null;
            foreach (string existingName in satellite.GetManifestResourceNames())
            {
                if (string.Equals(existingName, name, StringComparison.InvariantCultureIgnoreCase))
                {
                    if (canonicalName == null)
                    {
                        canonicalName = existingName;
                    }
                    else
                    {
                        throw new MissingManifestResourceException(SR.Format(SR.MissingManifestResource_MultipleBlobs, name, satellite.ToString()));
                    }
                }
            }
 
            if (canonicalName == null)
            {
                return null;
            }
 
            return satellite.GetManifestResourceStream(canonicalName);
        }
 
        private Assembly? GetSatelliteAssembly(CultureInfo lookForCulture)
        {
            Debug.Assert(_mediator.MainAssembly != null);
            if (!_mediator.LookedForSatelliteContractVersion)
            {
                _mediator.SatelliteContractVersion = ResourceManager.ResourceManagerMediator.ObtainSatelliteContractVersion(_mediator.MainAssembly);
                _mediator.LookedForSatelliteContractVersion = true;
            }
 
            Assembly? satellite = null;
 
            // Look up the satellite assembly, but don't let problems
            // like a partially signed satellite assembly stop us from
            // doing fallback and displaying something to the user.
            try
            {
                satellite = InternalGetSatelliteAssembly(_mediator.MainAssembly, lookForCulture, _mediator.SatelliteContractVersion);
            }
            catch (FileLoadException)
            {
            }
            catch (BadImageFormatException)
            {
                // Don't throw for zero-length satellite assemblies, for compat with v1
            }
 
            return satellite;
        }
 
        // Perf optimization - Don't use Reflection for most cases with
        // our .resources files.  This makes our code run faster and we can avoid
        // creating a ResourceReader via Reflection.  This would incur
        // a security check (since the link-time check on the constructor that
        // takes a String is turned into a full demand with a stack walk)
        // and causes partially trusted localized apps to fail.
        private bool CanUseDefaultResourceClasses(string readerTypeName, string resSetTypeName)
        {
            Debug.Assert(readerTypeName != null, "readerTypeName shouldn't be null; check caller");
            Debug.Assert(resSetTypeName != null, "resSetTypeName shouldn't be null; check caller");
 
            if (_mediator.UserResourceSet != null)
                return false;
 
            // Ignore the actual version of the ResourceReader and
            // RuntimeResourceSet classes.  Let those classes deal with
            // versioning themselves.
 
            if (readerTypeName != null)
            {
                if (!ResourceManager.IsDefaultType(readerTypeName, ResourceManager.ResReaderTypeName))
                    return false;
            }
 
            if (resSetTypeName != null)
            {
                if (!ResourceManager.IsDefaultType(resSetTypeName, ResourceManager.ResSetTypeName))
                    return false;
            }
 
            return true;
        }
 
        private void HandleSatelliteMissing()
        {
            Debug.Assert(_mediator.MainAssembly != null);
            AssemblyName mname = _mediator.MainAssembly.GetName();
            string satAssemName = AssemblyNameFormatter.ComputeDisplayName(
                mname.Name + ".resources.dll",
                _mediator.SatelliteContractVersion,
                null,
                mname.GetPublicKeyToken());
 
            Debug.Assert(_mediator.NeutralResourcesCulture != null);
            string missingCultureName = _mediator.NeutralResourcesCulture.Name;
            if (missingCultureName.Length == 0)
            {
                missingCultureName = "<invariant>";
            }
            throw new MissingSatelliteAssemblyException(SR.Format(SR.MissingSatelliteAssembly_Culture_Name, _mediator.NeutralResourcesCulture, satAssemName), missingCultureName);
        }
 
        private static string GetManifestResourceNamesList(Assembly assembly)
        {
            try
            {
                string[] resourceSetNames = assembly.GetManifestResourceNames();
                int length = resourceSetNames.Length;
                string postfix = "\"";
 
                // If we have more than 10 resource sets, we just print the first 10 for the sake of the exception message readability.
                const int MaxLength = 10;
                if (length > MaxLength)
                {
                    length = MaxLength;
                    postfix = "\", ...";
                }
 
                return "\"" + string.Join("\", \"", resourceSetNames, 0, length) + postfix;
            }
            catch
            {
                return "\"\"";
            }
        }
 
        private void HandleResourceStreamMissing(string fileName)
        {
            Debug.Assert(_mediator.BaseName != null);
            // Keep people from bothering me about resources problems
            if (_mediator.MainAssembly == typeof(object).Assembly && _mediator.BaseName.Equals(CoreLib.Name))
            {
                // This would break CultureInfo & all our exceptions.
                Debug.Fail("Couldn't get " + CoreLib.Name + ResourceManager.ResFileExtension + " from " + CoreLib.Name + "'s assembly" + Environment.NewLineConst + Environment.NewLineConst + "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.");
 
                // We cannot continue further - simply FailFast.
                const string MesgFailFast = System.CoreLib.Name + ResourceManager.ResFileExtension + " couldn't be found!  Large parts of the BCL won't work!";
                System.Environment.FailFast(MesgFailFast);
            }
            Debug.Assert(_mediator.MainAssembly != null);
            throw new MissingManifestResourceException(
                            SR.Format(SR.MissingManifestResource_NoNeutralAsm,
                            fileName, _mediator.MainAssembly.GetName().Name, GetManifestResourceNamesList(_mediator.MainAssembly)));
        }
    }
}