RuntimeResourceSet.cs 21 KB

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