summaryrefslogtreecommitdiff
path: root/src/mscorlib/shared/System/Resources/RuntimeResourceSet.cs
blob: 41d7541c2441364449627b61abc4857aad433f5e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
// 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.
**
** 
===========================================================*/

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;

namespace System.Resources
{
    // 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.
    // 
#if CORECLR
    internal
#else
    public  // On CoreRT, this must be public because of need to whitelist past the ReflectionBlock.
#endif
    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<String, ResourceLocator> _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<String, ResourceLocator> _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)
        {
            _resCache = new Dictionary<String, ResourceLocator>(FastResourceComparer.Default);
            Stream stream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read);
            _defaultReader = new ResourceReader(stream, _resCache);
            Reader = _defaultReader;
        }

        internal RuntimeResourceSet(Stream stream) : base(false)
        {
            _resCache = new Dictionary<String, ResourceLocator>(FastResourceComparer.Default);
            _defaultReader = new ResourceReader(stream, _resCache);
            Reader = _defaultReader;
        }

        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, SR.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, SR.ObjectDisposed_ResourceSet);
            Contract.EndContractBlock();

            Object value = null;
            ResourceLocator resLocation;

            lock (Reader)
            {
                if (Reader == null)
                    throw new ObjectDisposedException(null, SR.ObjectDisposed_ResourceSet);

                if (_defaultReader != null)
                {
                    // 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)
                    {
                        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<String, ResourceLocator>(StringComparer.OrdinalIgnoreCase);
                    }

                    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<String, ResourceLocator> 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;
                }
            }
            return value;
        }
    }
}