1 // ==++==
2 //
3 //   Copyright (c) Microsoft Corporation.  All rights reserved.
4 //
5 // ==--==
6 /*============================================================
7 **
8 ** Class:  RuntimeResourceSet
9 **
10 ** <OWNER>Microsoft</OWNER>
11 **
12 **
13 ** Purpose: CultureInfo-specific collection of resources.
14 **
15 **
16 ===========================================================*/
17 namespace System.Resources {
18     using System;
19     using System.IO;
20     using System.Collections;
21     using System.Collections.Generic;
22     using System.Globalization;
23     using System.Reflection;
24     using System.Runtime.Versioning;
25     using System.Diagnostics.Contracts;
26 
27     // A RuntimeResourceSet stores all the resources defined in one
28     // particular CultureInfo, with some loading optimizations.
29     //
30     // It is expected that nearly all the runtime's users will be satisfied with the
31     // default resource file format, and it will be more efficient than most simple
32     // implementations.  Users who would consider creating their own ResourceSets and/or
33     // ResourceReaders and ResourceWriters are people who have to interop with a
34     // legacy resource file format, are creating their own resource file format
35     // (using XML, for instance), or require doing resource lookups at runtime over
36     // the network.  This group will hopefully be small, but all the infrastructure
37     // should be in place to let these users write & plug in their own tools.
38     //
39     // The Default Resource File Format
40     //
41     // The fundamental problems addressed by the resource file format are:
42     //
43     // * Versioning - A ResourceReader could in theory support many different
44     // file format revisions.
45     // * Storing intrinsic datatypes (ie, ints, Strings, DateTimes, etc) in a compact
46     // format
47     // * Support for user-defined classes - Accomplished using Serialization
48     // * Resource lookups should not require loading an entire resource file - If you
49     // look up a resource, we only load the value for that resource, minimizing working set.
50     //
51     //
52     // There are four sections to the default file format.  The first
53     // is the Resource Manager header, which consists of a magic number
54     // that identifies this as a Resource file, and a ResourceSet class name.
55     // The class name is written here to allow users to provide their own
56     // implementation of a ResourceSet (and a matching ResourceReader) to
57     // control policy.  If objects greater than a certain size or matching a
58     // certain naming scheme shouldn't be stored in memory, users can tweak that
59     // with their own subclass of ResourceSet.
60     //
61     // The second section in the system default file format is the
62     // RuntimeResourceSet specific header.  This contains a version number for
63     // the .resources file, the number of resources in this file, the number of
64     // different types contained in the file, followed by a list of fully
65     // qualified type names.  After this, we include an array of hash values for
66     // each resource name, then an array of virtual offsets into the name section
67     // of the file.  The hashes allow us to do a binary search on an array of
68     // integers to find a resource name very quickly without doing many string
69     // compares (except for once we find the real type, of course).  If a hash
70     // matches, the index into the array of hash values is used as the index
71     // into the name position array to find the name of the resource.  The type
72     // table allows us to read multiple different classes from the same file,
73     // including user-defined types, in a more efficient way than using
74     // Serialization, at least when your .resources file contains a reasonable
75     // proportion of base data types such as Strings or ints.  We use
76     // Serialization for all the non-instrinsic types.
77     //
78     // The third section of the file is the name section.  It contains a
79     // series of resource names, written out as byte-length prefixed little
80     // endian Unicode strings (UTF-16).  After each name is a four byte virtual
81     // offset into the data section of the file, pointing to the relevant
82     // string or serialized blob for this resource name.
83     //
84     // The fourth section in the file is the data section, which consists
85     // of a type and a blob of bytes for each item in the file.  The type is
86     // an integer index into the type table.  The data is specific to that type,
87     // but may be a number written in binary format, a String, or a serialized
88     // Object.
89     //
90     // The system default file format (V1) is as follows:
91     //
92     //     What                                               Type of Data
93     // ====================================================   ===========
94     //
95     //                        Resource Manager header
96     // Magic Number (0xBEEFCACE)                              Int32
97     // Resource Manager header version                        Int32
98     // Num bytes to skip from here to get past this header    Int32
99     // Class name of IResourceReader to parse this file       String
100     // Class name of ResourceSet to parse this file           String
101     //
102     //                       RuntimeResourceReader header
103     // ResourceReader version number                          Int32
104     // [Only in debug V2 builds - "***DEBUG***"]              String
105     // Number of resources in the file                        Int32
106     // Number of types in the type table                      Int32
107     // Name of each type                                      Set of Strings
108     // Padding bytes for 8-byte alignment (use PAD)           Bytes (0-7)
109     // Hash values for each resource name                     Int32 array, sorted
110     // Virtual offset of each resource name                   Int32 array, coupled with hash values
111     // Absolute location of Data section                      Int32
112     //
113     //                     RuntimeResourceReader Name Section
114     // Name & virtual offset of each resource                 Set of (UTF-16 String, Int32) pairs
115     //
116     //                     RuntimeResourceReader Data Section
117     // Type and Value of each resource                Set of (Int32, blob of bytes) pairs
118     //
119     // This implementation, when used with the default ResourceReader class,
120     // loads only the strings that you look up for.  It can do string comparisons
121     // without having to create a new String instance due to some memory mapped
122     // file optimizations in the ResourceReader and FastResourceComparer
123     // classes.  This keeps the memory we touch to a minimum when loading
124     // resources.
125     //
126     // If you use a different IResourceReader class to read a file, or if you
127     // do case-insensitive lookups (and the case-sensitive lookup fails) then
128     // we will load all the names of each resource and each resource value.
129     // This could probably use some optimization.
130     //
131     // In addition, this supports object serialization in a similar fashion.
132     // We build an array of class types contained in this file, and write it
133     // to RuntimeResourceReader header section of the file.  Every resource
134     // will contain its type (as an index into the array of classes) with the data
135     // for that resource.  We will use the Runtime's serialization support for this.
136     //
137     // All strings in the file format are written with BinaryReader and
138     // BinaryWriter, which writes out the length of the String in bytes as an
139     // Int32 then the contents as Unicode chars encoded in UTF-8.  In the name
140     // table though, each resource name is written in UTF-16 so we can do a
141     // string compare byte by byte against the contents of the file, without
142     // allocating objects.  Ideally we'd have a way of comparing UTF-8 bytes
143     // directly against a String object, but that may be a lot of work.
144     //
145     // The offsets of each resource string are relative to the beginning
146     // of the Data section of the file.  This way, if a tool decided to add
147     // one resource to a file, it would only need to increment the number of
148     // resources, add the hash &amp; location of last byte in the name section
149     // to the array of resource hashes and resource name positions (carefully
150     // keeping these arrays sorted), add the name to the end of the name &amp;
151     // offset list, possibly add the type list of types types (and increase
152     // the number of items in the type table), and add the resource value at
153     // the end of the file.  The other offsets wouldn't need to be updated to
154     // reflect the longer header section.
155     //
156     // Resource files are currently limited to 2 gigabytes due to these
157     // design parameters.  A future version may raise the limit to 4 gigabytes
158     // by using unsigned integers, or may use negative numbers to load items
159     // out of an assembly manifest.  Also, we may try sectioning the resource names
160     // into smaller chunks, each of size sqrt(n), would be substantially better for
161     // resource files containing thousands of resources.
162     //
163     internal sealed class RuntimeResourceSet : ResourceSet, IEnumerable
164     {
165         internal const int Version = 2;            // File format version number
166 
167         // Cache for resources.  Key is the resource name, which can be cached
168         // for arbitrarily long times, since the object is usually a string
169         // literal that will live for the lifetime of the appdomain.  The
170         // value is a ResourceLocator instance, which might cache the object.
171         private Dictionary<String, ResourceLocator> _resCache;
172 
173 
174         // For our special load-on-demand reader, cache the cast.  The
175         // RuntimeResourceSet's implementation knows how to treat this reader specially.
176         private ResourceReader _defaultReader;
177 
178         // This is a lookup table for case-insensitive lookups, and may be null.
179         // Consider always using a case-insensitive resource cache, as we don't
180         // want to fill this out if we can avoid it.  The problem is resource
181         // fallback will somewhat regularly cause us to look up resources that
182         // don't exist.
183         private Dictionary<String, ResourceLocator> _caseInsensitiveTable;
184 
185         // If we're not using our custom reader, then enumerate through all
186         // the resources once, adding them into the table.
187         private bool _haveReadFromReader;
188 
189         [System.Security.SecurityCritical]  // auto-generated
190         [ResourceExposure(ResourceScope.Machine)]
191         [ResourceConsumption(ResourceScope.Machine)]
RuntimeResourceSet(String fileName)192         internal RuntimeResourceSet(String fileName) : base(false)
193         {
194             BCLDebug.Log("RESMGRFILEFORMAT", "RuntimeResourceSet .ctor(String)");
195             _resCache = new Dictionary<String, ResourceLocator>(FastResourceComparer.Default);
196             Stream stream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read);
197             _defaultReader = new ResourceReader(stream, _resCache);
198             Reader = _defaultReader;
199         }
200 
201 #if LOOSELY_LINKED_RESOURCE_REFERENCE
RuntimeResourceSet(Stream stream, Assembly assembly)202         internal RuntimeResourceSet(Stream stream, Assembly assembly) : base(false)
203         {
204             BCLDebug.Log("RESMGRFILEFORMAT", "RuntimeResourceSet .ctor(Stream)");
205             _resCache = new Dictionary<String, ResourceLocator>(FastResourceComparer.Default);
206             _defaultReader = new ResourceReader(stream, _resCache);
207             Reader = _defaultReader;
208             Assembly = assembly;
209         }
210 #else
211         [System.Security.SecurityCritical]  // auto-generated
RuntimeResourceSet(Stream stream)212         internal RuntimeResourceSet(Stream stream) : base(false)
213         {
214             BCLDebug.Log("RESMGRFILEFORMAT", "RuntimeResourceSet .ctor(Stream)");
215             _resCache = new Dictionary<String, ResourceLocator>(FastResourceComparer.Default);
216             _defaultReader = new ResourceReader(stream, _resCache);
217             Reader = _defaultReader;
218         }
219 #endif // LOOSELY_LINKED_RESOURCE_REFERENCE
220 
Dispose(bool disposing)221         protected override void Dispose(bool disposing)
222         {
223             if (Reader == null)
224                 return;
225 
226             if (disposing) {
227                 lock(Reader) {
228                     _resCache = null;
229                     if (_defaultReader != null) {
230                         _defaultReader.Close();
231                         _defaultReader = null;
232                     }
233                     _caseInsensitiveTable = null;
234                     // Set Reader to null to avoid a race in GetObject.
235                     base.Dispose(disposing);
236                 }
237             }
238             else {
239                 // Just to make sure we always clear these fields in the future...
240                 _resCache = null;
241                 _caseInsensitiveTable = null;
242                 _defaultReader = null;
243                 base.Dispose(disposing);
244             }
245         }
246 
GetEnumerator()247         public override IDictionaryEnumerator GetEnumerator()
248         {
249             return GetEnumeratorHelper();
250         }
251 
IEnumerable.GetEnumerator()252         IEnumerator IEnumerable.GetEnumerator()
253         {
254             return GetEnumeratorHelper();
255         }
256 
GetEnumeratorHelper()257         private IDictionaryEnumerator GetEnumeratorHelper()
258         {
259             IResourceReader copyOfReader = Reader;
260             if (copyOfReader == null || _resCache == null)
261                 throw new ObjectDisposedException(null, Environment.GetResourceString("ObjectDisposed_ResourceSet"));
262 
263             return copyOfReader.GetEnumerator();
264         }
265 
266 
GetString(String key)267         public override String GetString(String key)
268         {
269             Object o = GetObject(key, false, true);
270             return (String) o;
271         }
272 
GetString(String key, bool ignoreCase)273         public override String GetString(String key, bool ignoreCase)
274         {
275             Object o = GetObject(key, ignoreCase, true);
276             return (String) o;
277         }
278 
GetObject(String key)279         public override Object GetObject(String key)
280         {
281             return GetObject(key, false, false);
282         }
283 
GetObject(String key, bool ignoreCase)284         public override Object GetObject(String key, bool ignoreCase)
285         {
286             return GetObject(key, ignoreCase, false);
287         }
288 
GetObject(String key, bool ignoreCase, bool isString)289         private Object GetObject(String key, bool ignoreCase, bool isString)
290         {
291             if (key==null)
292                 throw new ArgumentNullException("key");
293             if (Reader == null || _resCache == null)
294                 throw new ObjectDisposedException(null, Environment.GetResourceString("ObjectDisposed_ResourceSet"));
295             Contract.EndContractBlock();
296 
297             Object value = null;
298             ResourceLocator resLocation;
299 
300             lock(Reader) {
301                 if (Reader == null)
302                     throw new ObjectDisposedException(null, Environment.GetResourceString("ObjectDisposed_ResourceSet"));
303 
304                 if (_defaultReader != null) {
305                     BCLDebug.Log("RESMGRFILEFORMAT", "Going down fast path in RuntimeResourceSet::GetObject");
306 
307                     // Find the offset within the data section
308                     int dataPos = -1;
309                     if (_resCache.TryGetValue(key, out resLocation)) {
310                         value = resLocation.Value;
311                         dataPos = resLocation.DataPosition;
312                     }
313 
314                     if (dataPos == -1 && value == null) {
315                         dataPos = _defaultReader.FindPosForResource(key);
316                     }
317 
318                     if (dataPos != -1 && value == null) {
319                         Contract.Assert(dataPos >= 0, "data section offset cannot be negative!");
320                         // Normally calling LoadString or LoadObject requires
321                         // taking a lock.  Note that in this case, we took a
322                         // lock on the entire RuntimeResourceSet, which is
323                         // sufficient since we never pass this ResourceReader
324                         // to anyone else.
325                         ResourceTypeCode typeCode;
326                         if (isString) {
327                             value = _defaultReader.LoadString(dataPos);
328                             typeCode = ResourceTypeCode.String;
329                         }
330                         else {
331                             value = _defaultReader.LoadObject(dataPos, out typeCode);
332                         }
333 
334                         resLocation = new ResourceLocator(dataPos, (ResourceLocator.CanCache(typeCode)) ? value : null);
335                         lock(_resCache) {
336                             _resCache[key] = resLocation;
337                         }
338                     }
339 
340                     if (value != null || !ignoreCase) {
341 #if LOOSELY_LINKED_RESOURCE_REFERENCE
342                         if (Assembly != null && (value is LooselyLinkedResourceReference)) {
343                             LooselyLinkedResourceReference assRef = (LooselyLinkedResourceReference) value;
344                             value = assRef.Resolve(Assembly);
345                         }
346 #endif // LOOSELY_LINKED_RESOURCE_REFERENCE
347 
348                         return value;  // may be null
349                     }
350                 }  // if (_defaultReader != null)
351 
352                 // At this point, we either don't have our default resource reader
353                 // or we haven't found the particular resource we're looking for
354                 // and may have to search for it in a case-insensitive way.
355                 if (!_haveReadFromReader) {
356                     // If necessary, init our case insensitive hash table.
357                     if (ignoreCase && _caseInsensitiveTable == null) {
358                         _caseInsensitiveTable = new Dictionary<String, ResourceLocator>(StringComparer.OrdinalIgnoreCase);
359                     }
360 #if _DEBUG
361                     BCLDebug.Perf(!ignoreCase, "Using case-insensitive lookups is bad perf-wise.  Consider capitalizing "+key+" correctly in your source");
362 #endif
363 
364                     if (_defaultReader == null) {
365                         IDictionaryEnumerator en = Reader.GetEnumerator();
366                         while (en.MoveNext()) {
367                             DictionaryEntry entry = en.Entry;
368                             String readKey = (String) entry.Key;
369                             ResourceLocator resLoc = new ResourceLocator(-1, entry.Value);
370                             _resCache.Add(readKey, resLoc);
371                             if (ignoreCase)
372                                 _caseInsensitiveTable.Add(readKey, resLoc);
373                         }
374                         // Only close the reader if it is NOT our default one,
375                         // since we need it around to resolve ResourceLocators.
376                         if (!ignoreCase)
377                             Reader.Close();
378                     }
379                     else {
380                         Contract.Assert(ignoreCase, "This should only happen for case-insensitive lookups");
381                         ResourceReader.ResourceEnumerator en = _defaultReader.GetEnumeratorInternal();
382                         while (en.MoveNext()) {
383                             // Note: Always ask for the resource key before the data position.
384                             String currentKey = (String) en.Key;
385                             int dataPos = en.DataPosition;
386                             ResourceLocator resLoc = new ResourceLocator(dataPos, null);
387                             _caseInsensitiveTable.Add(currentKey, resLoc);
388                         }
389                     }
390                     _haveReadFromReader = true;
391                 }
392                 Object obj = null;
393                 bool found = false;
394                 bool keyInWrongCase = false;
395                 if (_defaultReader != null) {
396                     if (_resCache.TryGetValue(key, out resLocation)) {
397                         found = true;
398                         obj = ResolveResourceLocator(resLocation, key, _resCache, keyInWrongCase);
399                     }
400                 }
401                 if (!found && ignoreCase) {
402                     if (_caseInsensitiveTable.TryGetValue(key, out resLocation)) {
403                         found = true;
404                         keyInWrongCase = true;
405                         obj = ResolveResourceLocator(resLocation, key, _resCache, keyInWrongCase);
406                     }
407                 }
408                 return obj;
409             } // lock(Reader)
410         }
411 
412         // The last parameter indicates whether the lookup required a
413         // case-insensitive lookup to succeed, indicating we shouldn't add
414         // the ResourceLocation to our case-sensitive cache.
ResolveResourceLocator(ResourceLocator resLocation, String key, Dictionary<String, ResourceLocator> copyOfCache, bool keyInWrongCase)415         private Object ResolveResourceLocator(ResourceLocator resLocation, String key, Dictionary<String, ResourceLocator> copyOfCache, bool keyInWrongCase)
416         {
417             // We need to explicitly resolve loosely linked manifest
418             // resources, and we need to resolve ResourceLocators with null objects.
419             Object value = resLocation.Value;
420             if (value == null) {
421                 ResourceTypeCode typeCode;
422                 lock(Reader) {
423                     value = _defaultReader.LoadObject(resLocation.DataPosition, out typeCode);
424                 }
425                 if (!keyInWrongCase && ResourceLocator.CanCache(typeCode)) {
426                     resLocation.Value = value;
427                     copyOfCache[key] = resLocation;
428                 }
429             }
430 #if LOOSELY_LINKED_RESOURCE_REFERENCE
431             if (Assembly != null && value is LooselyLinkedResourceReference) {
432                 LooselyLinkedResourceReference assRef = (LooselyLinkedResourceReference) value;
433                 value = assRef.Resolve(Assembly);
434             }
435 #endif // LOOSELY_LINKED_RESOURCE_REFERENCE
436             return value;
437         }
438     }
439 }
440