// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. /*============================================================ ** ** ** ** ** ** Purpose: CultureInfo-specific collection of resources. ** ** ===========================================================*/ namespace System.Resources { using System; using System.IO; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Reflection; using System.Runtime.Versioning; using System.Diagnostics; using System.Diagnostics.Contracts; // A RuntimeResourceSet stores all the resources defined in one // particular CultureInfo, with some loading optimizations. // // It is expected that nearly all the runtime's users will be satisfied with the // default resource file format, and it will be more efficient than most simple // implementations. Users who would consider creating their own ResourceSets and/or // ResourceReaders and ResourceWriters are people who have to interop with a // legacy resource file format, are creating their own resource file format // (using XML, for instance), or require doing resource lookups at runtime over // the network. This group will hopefully be small, but all the infrastructure // should be in place to let these users write & plug in their own tools. // // The Default Resource File Format // // The fundamental problems addressed by the resource file format are: // // * Versioning - A ResourceReader could in theory support many different // file format revisions. // * Storing intrinsic datatypes (ie, ints, Strings, DateTimes, etc) in a compact // format // * Support for user-defined classes - Accomplished using Serialization // * Resource lookups should not require loading an entire resource file - If you // look up a resource, we only load the value for that resource, minimizing working set. // // // There are four sections to the default file format. The first // is the Resource Manager header, which consists of a magic number // that identifies this as a Resource file, and a ResourceSet class name. // The class name is written here to allow users to provide their own // implementation of a ResourceSet (and a matching ResourceReader) to // control policy. If objects greater than a certain size or matching a // certain naming scheme shouldn't be stored in memory, users can tweak that // with their own subclass of ResourceSet. // // The second section in the system default file format is the // RuntimeResourceSet specific header. This contains a version number for // the .resources file, the number of resources in this file, the number of // different types contained in the file, followed by a list of fully // qualified type names. After this, we include an array of hash values for // each resource name, then an array of virtual offsets into the name section // of the file. The hashes allow us to do a binary search on an array of // integers to find a resource name very quickly without doing many string // compares (except for once we find the real type, of course). If a hash // matches, the index into the array of hash values is used as the index // into the name position array to find the name of the resource. The type // table allows us to read multiple different classes from the same file, // including user-defined types, in a more efficient way than using // Serialization, at least when your .resources file contains a reasonable // proportion of base data types such as Strings or ints. We use // Serialization for all the non-instrinsic types. // // The third section of the file is the name section. It contains a // series of resource names, written out as byte-length prefixed little // endian Unicode strings (UTF-16). After each name is a four byte virtual // offset into the data section of the file, pointing to the relevant // string or serialized blob for this resource name. // // The fourth section in the file is the data section, which consists // of a type and a blob of bytes for each item in the file. The type is // an integer index into the type table. The data is specific to that type, // but may be a number written in binary format, a String, or a serialized // Object. // // The system default file format (V1) is as follows: // // What Type of Data // ==================================================== =========== // // Resource Manager header // Magic Number (0xBEEFCACE) Int32 // Resource Manager header version Int32 // Num bytes to skip from here to get past this header Int32 // Class name of IResourceReader to parse this file String // Class name of ResourceSet to parse this file String // // RuntimeResourceReader header // ResourceReader version number Int32 // [Only in debug V2 builds - "***DEBUG***"] String // Number of resources in the file Int32 // Number of types in the type table Int32 // Name of each type Set of Strings // Padding bytes for 8-byte alignment (use PAD) Bytes (0-7) // Hash values for each resource name Int32 array, sorted // Virtual offset of each resource name Int32 array, coupled with hash values // Absolute location of Data section Int32 // // RuntimeResourceReader Name Section // Name & virtual offset of each resource Set of (UTF-16 String, Int32) pairs // // RuntimeResourceReader Data Section // Type and Value of each resource Set of (Int32, blob of bytes) pairs // // This implementation, when used with the default ResourceReader class, // loads only the strings that you look up for. It can do string comparisons // without having to create a new String instance due to some memory mapped // file optimizations in the ResourceReader and FastResourceComparer // classes. This keeps the memory we touch to a minimum when loading // resources. // // If you use a different IResourceReader class to read a file, or if you // do case-insensitive lookups (and the case-sensitive lookup fails) then // we will load all the names of each resource and each resource value. // This could probably use some optimization. // // In addition, this supports object serialization in a similar fashion. // We build an array of class types contained in this file, and write it // to RuntimeResourceReader header section of the file. Every resource // will contain its type (as an index into the array of classes) with the data // for that resource. We will use the Runtime's serialization support for this. // // All strings in the file format are written with BinaryReader and // BinaryWriter, which writes out the length of the String in bytes as an // Int32 then the contents as Unicode chars encoded in UTF-8. In the name // table though, each resource name is written in UTF-16 so we can do a // string compare byte by byte against the contents of the file, without // allocating objects. Ideally we'd have a way of comparing UTF-8 bytes // directly against a String object, but that may be a lot of work. // // The offsets of each resource string are relative to the beginning // of the Data section of the file. This way, if a tool decided to add // one resource to a file, it would only need to increment the number of // resources, add the hash & location of last byte in the name section // to the array of resource hashes and resource name positions (carefully // keeping these arrays sorted), add the name to the end of the name & // offset list, possibly add the type list of types types (and increase // the number of items in the type table), and add the resource value at // the end of the file. The other offsets wouldn't need to be updated to // reflect the longer header section. // // Resource files are currently limited to 2 gigabytes due to these // design parameters. A future version may raise the limit to 4 gigabytes // by using unsigned integers, or may use negative numbers to load items // out of an assembly manifest. Also, we may try sectioning the resource names // into smaller chunks, each of size sqrt(n), would be substantially better for // resource files containing thousands of resources. // internal sealed class RuntimeResourceSet : ResourceSet, IEnumerable { internal const int Version = 2; // File format version number // Cache for resources. Key is the resource name, which can be cached // for arbitrarily long times, since the object is usually a string // literal that will live for the lifetime of the appdomain. The // value is a ResourceLocator instance, which might cache the object. private Dictionary _resCache; // For our special load-on-demand reader, cache the cast. The // RuntimeResourceSet's implementation knows how to treat this reader specially. private ResourceReader _defaultReader; // This is a lookup table for case-insensitive lookups, and may be null. // Consider always using a case-insensitive resource cache, as we don't // want to fill this out if we can avoid it. The problem is resource // fallback will somewhat regularly cause us to look up resources that // don't exist. private Dictionary _caseInsensitiveTable; // If we're not using our custom reader, then enumerate through all // the resources once, adding them into the table. private bool _haveReadFromReader; internal RuntimeResourceSet(String fileName) : base(false) { BCLDebug.Log("RESMGRFILEFORMAT", "RuntimeResourceSet .ctor(String)"); _resCache = new Dictionary(FastResourceComparer.Default); Stream stream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read); _defaultReader = new ResourceReader(stream, _resCache); Reader = _defaultReader; } #if LOOSELY_LINKED_RESOURCE_REFERENCE internal RuntimeResourceSet(Stream stream, Assembly assembly) : base(false) { BCLDebug.Log("RESMGRFILEFORMAT", "RuntimeResourceSet .ctor(Stream)"); _resCache = new Dictionary(FastResourceComparer.Default); _defaultReader = new ResourceReader(stream, _resCache); Reader = _defaultReader; Assembly = assembly; } #else internal RuntimeResourceSet(Stream stream) : base(false) { BCLDebug.Log("RESMGRFILEFORMAT", "RuntimeResourceSet .ctor(Stream)"); _resCache = new Dictionary(FastResourceComparer.Default); _defaultReader = new ResourceReader(stream, _resCache); Reader = _defaultReader; } #endif // LOOSELY_LINKED_RESOURCE_REFERENCE protected override void Dispose(bool disposing) { if (Reader == null) return; if (disposing) { lock (Reader) { _resCache = null; if (_defaultReader != null) { _defaultReader.Close(); _defaultReader = null; } _caseInsensitiveTable = null; // Set Reader to null to avoid a race in GetObject. base.Dispose(disposing); } } else { // Just to make sure we always clear these fields in the future... _resCache = null; _caseInsensitiveTable = null; _defaultReader = null; base.Dispose(disposing); } } public override IDictionaryEnumerator GetEnumerator() { return GetEnumeratorHelper(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumeratorHelper(); } private IDictionaryEnumerator GetEnumeratorHelper() { IResourceReader copyOfReader = Reader; if (copyOfReader == null || _resCache == null) throw new ObjectDisposedException(null, Environment.GetResourceString("ObjectDisposed_ResourceSet")); return copyOfReader.GetEnumerator(); } public override String GetString(String key) { Object o = GetObject(key, false, true); return (String)o; } public override String GetString(String key, bool ignoreCase) { Object o = GetObject(key, ignoreCase, true); return (String)o; } public override Object GetObject(String key) { return GetObject(key, false, false); } public override Object GetObject(String key, bool ignoreCase) { return GetObject(key, ignoreCase, false); } private Object GetObject(String key, bool ignoreCase, bool isString) { if (key == null) throw new ArgumentNullException(nameof(key)); if (Reader == null || _resCache == null) throw new ObjectDisposedException(null, Environment.GetResourceString("ObjectDisposed_ResourceSet")); Contract.EndContractBlock(); Object value = null; ResourceLocator resLocation; lock (Reader) { if (Reader == null) throw new ObjectDisposedException(null, Environment.GetResourceString("ObjectDisposed_ResourceSet")); if (_defaultReader != null) { BCLDebug.Log("RESMGRFILEFORMAT", "Going down fast path in RuntimeResourceSet::GetObject"); // Find the offset within the data section int dataPos = -1; if (_resCache.TryGetValue(key, out resLocation)) { value = resLocation.Value; dataPos = resLocation.DataPosition; } if (dataPos == -1 && value == null) { dataPos = _defaultReader.FindPosForResource(key); } if (dataPos != -1 && value == null) { Debug.Assert(dataPos >= 0, "data section offset cannot be negative!"); // Normally calling LoadString or LoadObject requires // taking a lock. Note that in this case, we took a // lock on the entire RuntimeResourceSet, which is // sufficient since we never pass this ResourceReader // to anyone else. ResourceTypeCode typeCode; if (isString) { value = _defaultReader.LoadString(dataPos); typeCode = ResourceTypeCode.String; } else { value = _defaultReader.LoadObject(dataPos, out typeCode); } resLocation = new ResourceLocator(dataPos, (ResourceLocator.CanCache(typeCode)) ? value : null); lock (_resCache) { _resCache[key] = resLocation; } } if (value != null || !ignoreCase) { #if LOOSELY_LINKED_RESOURCE_REFERENCE if (Assembly != null && (value is LooselyLinkedResourceReference)) { LooselyLinkedResourceReference assRef = (LooselyLinkedResourceReference) value; value = assRef.Resolve(Assembly); } #endif // LOOSELY_LINKED_RESOURCE_REFERENCE return value; // may be null } } // if (_defaultReader != null) // At this point, we either don't have our default resource reader // or we haven't found the particular resource we're looking for // and may have to search for it in a case-insensitive way. if (!_haveReadFromReader) { // If necessary, init our case insensitive hash table. if (ignoreCase && _caseInsensitiveTable == null) { _caseInsensitiveTable = new Dictionary(StringComparer.OrdinalIgnoreCase); } #if _DEBUG BCLDebug.Perf(!ignoreCase, "Using case-insensitive lookups is bad perf-wise. Consider capitalizing " + key + " correctly in your source"); #endif if (_defaultReader == null) { IDictionaryEnumerator en = Reader.GetEnumerator(); while (en.MoveNext()) { DictionaryEntry entry = en.Entry; String readKey = (String)entry.Key; ResourceLocator resLoc = new ResourceLocator(-1, entry.Value); _resCache.Add(readKey, resLoc); if (ignoreCase) _caseInsensitiveTable.Add(readKey, resLoc); } // Only close the reader if it is NOT our default one, // since we need it around to resolve ResourceLocators. if (!ignoreCase) Reader.Close(); } else { Debug.Assert(ignoreCase, "This should only happen for case-insensitive lookups"); ResourceReader.ResourceEnumerator en = _defaultReader.GetEnumeratorInternal(); while (en.MoveNext()) { // Note: Always ask for the resource key before the data position. String currentKey = (String)en.Key; int dataPos = en.DataPosition; ResourceLocator resLoc = new ResourceLocator(dataPos, null); _caseInsensitiveTable.Add(currentKey, resLoc); } } _haveReadFromReader = true; } Object obj = null; bool found = false; bool keyInWrongCase = false; if (_defaultReader != null) { if (_resCache.TryGetValue(key, out resLocation)) { found = true; obj = ResolveResourceLocator(resLocation, key, _resCache, keyInWrongCase); } } if (!found && ignoreCase) { if (_caseInsensitiveTable.TryGetValue(key, out resLocation)) { found = true; keyInWrongCase = true; obj = ResolveResourceLocator(resLocation, key, _resCache, keyInWrongCase); } } return obj; } // lock(Reader) } // The last parameter indicates whether the lookup required a // case-insensitive lookup to succeed, indicating we shouldn't add // the ResourceLocation to our case-sensitive cache. private Object ResolveResourceLocator(ResourceLocator resLocation, String key, Dictionary copyOfCache, bool keyInWrongCase) { // We need to explicitly resolve loosely linked manifest // resources, and we need to resolve ResourceLocators with null objects. Object value = resLocation.Value; if (value == null) { ResourceTypeCode typeCode; lock (Reader) { value = _defaultReader.LoadObject(resLocation.DataPosition, out typeCode); } if (!keyInWrongCase && ResourceLocator.CanCache(typeCode)) { resLocation.Value = value; copyOfCache[key] = resLocation; } } #if LOOSELY_LINKED_RESOURCE_REFERENCE if (Assembly != null && value is LooselyLinkedResourceReference) { LooselyLinkedResourceReference assRef = (LooselyLinkedResourceReference) value; value = assRef.Resolve(Assembly); } #endif // LOOSELY_LINKED_RESOURCE_REFERENCE return value; } } }