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: CultureInfo-specific collection of resources.
12 **
13 **
14 ===========================================================*/
15 
16 using System;
17 using System.IO;
18 using System.Collections;
19 using System.Collections.Generic;
20 using System.Globalization;
21 using System.Reflection;
22 using System.Runtime.Versioning;
23 using System.Diagnostics;
24 
25 namespace System.Resources
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 & 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 &
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 #if CORERT
164     public  // On CoreRT, this must be public because of need to whitelist past the ReflectionBlock.
165 #else
166     internal
167 #endif
168     sealed class RuntimeResourceSet : ResourceSet, IEnumerable
169     {
170         internal const int Version = 2;            // File format version number
171 
172         // Cache for resources.  Key is the resource name, which can be cached
173         // for arbitrarily long times, since the object is usually a string
174         // literal that will live for the lifetime of the appdomain.  The
175         // value is a ResourceLocator instance, which might cache the object.
176         private Dictionary<String, ResourceLocator> _resCache;
177 
178 
179         // For our special load-on-demand reader, cache the cast.  The
180         // RuntimeResourceSet's implementation knows how to treat this reader specially.
181         private ResourceReader _defaultReader;
182 
183         // This is a lookup table for case-insensitive lookups, and may be null.
184         // Consider always using a case-insensitive resource cache, as we don't
185         // want to fill this out if we can avoid it.  The problem is resource
186         // fallback will somewhat regularly cause us to look up resources that
187         // don't exist.
188         private Dictionary<String, ResourceLocator> _caseInsensitiveTable;
189 
190         // If we're not using our custom reader, then enumerate through all
191         // the resources once, adding them into the table.
192         private bool _haveReadFromReader;
193 
RuntimeResourceSet(String fileName)194         internal RuntimeResourceSet(String fileName) : base(false)
195         {
196             _resCache = new Dictionary<String, ResourceLocator>(FastResourceComparer.Default);
197             Stream stream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read);
198             _defaultReader = new ResourceReader(stream, _resCache);
199             Reader = _defaultReader;
200         }
201 
RuntimeResourceSet(Stream stream)202         internal RuntimeResourceSet(Stream stream) : base(false)
203         {
204             _resCache = new Dictionary<String, ResourceLocator>(FastResourceComparer.Default);
205             _defaultReader = new ResourceReader(stream, _resCache);
206             Reader = _defaultReader;
207         }
208 
Dispose(bool disposing)209         protected override void Dispose(bool disposing)
210         {
211             if (Reader == null)
212                 return;
213 
214             if (disposing)
215             {
216                 lock (Reader)
217                 {
218                     _resCache = null;
219                     if (_defaultReader != null)
220                     {
221                         _defaultReader.Close();
222                         _defaultReader = null;
223                     }
224                     _caseInsensitiveTable = null;
225                     // Set Reader to null to avoid a race in GetObject.
226                     base.Dispose(disposing);
227                 }
228             }
229             else
230             {
231                 // Just to make sure we always clear these fields in the future...
232                 _resCache = null;
233                 _caseInsensitiveTable = null;
234                 _defaultReader = null;
235                 base.Dispose(disposing);
236             }
237         }
238 
GetEnumerator()239         public override IDictionaryEnumerator GetEnumerator()
240         {
241             return GetEnumeratorHelper();
242         }
243 
IEnumerable.GetEnumerator()244         IEnumerator IEnumerable.GetEnumerator()
245         {
246             return GetEnumeratorHelper();
247         }
248 
GetEnumeratorHelper()249         private IDictionaryEnumerator GetEnumeratorHelper()
250         {
251             IResourceReader copyOfReader = Reader;
252             if (copyOfReader == null || _resCache == null)
253                 throw new ObjectDisposedException(null, SR.ObjectDisposed_ResourceSet);
254 
255             return copyOfReader.GetEnumerator();
256         }
257 
258 
GetString(String key)259         public override String GetString(String key)
260         {
261             Object o = GetObject(key, false, true);
262             return (String)o;
263         }
264 
GetString(String key, bool ignoreCase)265         public override String GetString(String key, bool ignoreCase)
266         {
267             Object o = GetObject(key, ignoreCase, true);
268             return (String)o;
269         }
270 
GetObject(String key)271         public override Object GetObject(String key)
272         {
273             return GetObject(key, false, false);
274         }
275 
GetObject(String key, bool ignoreCase)276         public override Object GetObject(String key, bool ignoreCase)
277         {
278             return GetObject(key, ignoreCase, false);
279         }
280 
GetObject(String key, bool ignoreCase, bool isString)281         private Object GetObject(String key, bool ignoreCase, bool isString)
282         {
283             if (key == null)
284                 throw new ArgumentNullException(nameof(key));
285             if (Reader == null || _resCache == null)
286                 throw new ObjectDisposedException(null, SR.ObjectDisposed_ResourceSet);
287 
288             Object value = null;
289             ResourceLocator resLocation;
290 
291             lock (Reader)
292             {
293                 if (Reader == null)
294                     throw new ObjectDisposedException(null, SR.ObjectDisposed_ResourceSet);
295 
296                 if (_defaultReader != null)
297                 {
298                     // Find the offset within the data section
299                     int dataPos = -1;
300                     if (_resCache.TryGetValue(key, out resLocation))
301                     {
302                         value = resLocation.Value;
303                         dataPos = resLocation.DataPosition;
304                     }
305 
306                     if (dataPos == -1 && value == null)
307                     {
308                         dataPos = _defaultReader.FindPosForResource(key);
309                     }
310 
311                     if (dataPos != -1 && value == null)
312                     {
313                         Debug.Assert(dataPos >= 0, "data section offset cannot be negative!");
314                         // Normally calling LoadString or LoadObject requires
315                         // taking a lock.  Note that in this case, we took a
316                         // lock on the entire RuntimeResourceSet, which is
317                         // sufficient since we never pass this ResourceReader
318                         // to anyone else.
319                         ResourceTypeCode typeCode;
320                         if (isString)
321                         {
322                             value = _defaultReader.LoadString(dataPos);
323                             typeCode = ResourceTypeCode.String;
324                         }
325                         else
326                         {
327                             value = _defaultReader.LoadObject(dataPos, out typeCode);
328                         }
329 
330                         resLocation = new ResourceLocator(dataPos, (ResourceLocator.CanCache(typeCode)) ? value : null);
331                         lock (_resCache)
332                         {
333                             _resCache[key] = resLocation;
334                         }
335                     }
336 
337                     if (value != null || !ignoreCase)
338                     {
339                         return value;  // may be null
340                     }
341                 }  // if (_defaultReader != null)
342 
343                 // At this point, we either don't have our default resource reader
344                 // or we haven't found the particular resource we're looking for
345                 // and may have to search for it in a case-insensitive way.
346                 if (!_haveReadFromReader)
347                 {
348                     // If necessary, init our case insensitive hash table.
349                     if (ignoreCase && _caseInsensitiveTable == null)
350                     {
351                         _caseInsensitiveTable = new Dictionary<String, ResourceLocator>(StringComparer.OrdinalIgnoreCase);
352                     }
353 
354                     if (_defaultReader == null)
355                     {
356                         IDictionaryEnumerator en = Reader.GetEnumerator();
357                         while (en.MoveNext())
358                         {
359                             DictionaryEntry entry = en.Entry;
360                             String readKey = (String)entry.Key;
361                             ResourceLocator resLoc = new ResourceLocator(-1, entry.Value);
362                             _resCache.Add(readKey, resLoc);
363                             if (ignoreCase)
364                                 _caseInsensitiveTable.Add(readKey, resLoc);
365                         }
366                         // Only close the reader if it is NOT our default one,
367                         // since we need it around to resolve ResourceLocators.
368                         if (!ignoreCase)
369                             Reader.Close();
370                     }
371                     else
372                     {
373                         Debug.Assert(ignoreCase, "This should only happen for case-insensitive lookups");
374                         ResourceReader.ResourceEnumerator en = _defaultReader.GetEnumeratorInternal();
375                         while (en.MoveNext())
376                         {
377                             // Note: Always ask for the resource key before the data position.
378                             String currentKey = (String)en.Key;
379                             int dataPos = en.DataPosition;
380                             ResourceLocator resLoc = new ResourceLocator(dataPos, null);
381                             _caseInsensitiveTable.Add(currentKey, resLoc);
382                         }
383                     }
384                     _haveReadFromReader = true;
385                 }
386                 Object obj = null;
387                 bool found = false;
388                 bool keyInWrongCase = false;
389                 if (_defaultReader != null)
390                 {
391                     if (_resCache.TryGetValue(key, out resLocation))
392                     {
393                         found = true;
394                         obj = ResolveResourceLocator(resLocation, key, _resCache, keyInWrongCase);
395                     }
396                 }
397                 if (!found && ignoreCase)
398                 {
399                     if (_caseInsensitiveTable.TryGetValue(key, out resLocation))
400                     {
401                         found = true;
402                         keyInWrongCase = true;
403                         obj = ResolveResourceLocator(resLocation, key, _resCache, keyInWrongCase);
404                     }
405                 }
406                 return obj;
407             } // lock(Reader)
408         }
409 
410         // The last parameter indicates whether the lookup required a
411         // case-insensitive lookup to succeed, indicating we shouldn't add
412         // the ResourceLocation to our case-sensitive cache.
ResolveResourceLocator(ResourceLocator resLocation, String key, Dictionary<String, ResourceLocator> copyOfCache, bool keyInWrongCase)413         private Object ResolveResourceLocator(ResourceLocator resLocation, String key, Dictionary<String, ResourceLocator> copyOfCache, bool keyInWrongCase)
414         {
415             // We need to explicitly resolve loosely linked manifest
416             // resources, and we need to resolve ResourceLocators with null objects.
417             Object value = resLocation.Value;
418             if (value == null)
419             {
420                 ResourceTypeCode typeCode;
421                 lock (Reader)
422                 {
423                     value = _defaultReader.LoadObject(resLocation.DataPosition, out typeCode);
424                 }
425                 if (!keyInWrongCase && ResourceLocator.CanCache(typeCode))
426                 {
427                     resLocation.Value = value;
428                     copyOfCache[key] = resLocation;
429                 }
430             }
431             return value;
432         }
433     }
434 }
435