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