440 lines
21 KiB
C#
440 lines
21 KiB
C#
// ==++==
|
|
//
|
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
//
|
|
// ==--==
|
|
/*============================================================
|
|
**
|
|
** Class: RuntimeResourceSet
|
|
**
|
|
** <OWNER>[....]</OWNER>
|
|
**
|
|
**
|
|
** Purpose: CultureInfo-specific collection of resources.
|
|
**
|
|
**
|
|
===========================================================*/
|
|
namespace System.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.Contracts;
|
|
|
|
// 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.
|
|
//
|
|
internal 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;
|
|
|
|
[System.Security.SecurityCritical] // auto-generated
|
|
[ResourceExposure(ResourceScope.Machine)]
|
|
[ResourceConsumption(ResourceScope.Machine)]
|
|
internal RuntimeResourceSet(String fileName) : base(false)
|
|
{
|
|
BCLDebug.Log("RESMGRFILEFORMAT", "RuntimeResourceSet .ctor(String)");
|
|
_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;
|
|
}
|
|
|
|
#if LOOSELY_LINKED_RESOURCE_REFERENCE
|
|
internal RuntimeResourceSet(Stream stream, Assembly assembly) : base(false)
|
|
{
|
|
BCLDebug.Log("RESMGRFILEFORMAT", "RuntimeResourceSet .ctor(Stream)");
|
|
_resCache = new Dictionary<String, ResourceLocator>(FastResourceComparer.Default);
|
|
_defaultReader = new ResourceReader(stream, _resCache);
|
|
Reader = _defaultReader;
|
|
Assembly = assembly;
|
|
}
|
|
#else
|
|
[System.Security.SecurityCritical] // auto-generated
|
|
internal RuntimeResourceSet(Stream stream) : base(false)
|
|
{
|
|
BCLDebug.Log("RESMGRFILEFORMAT", "RuntimeResourceSet .ctor(Stream)");
|
|
_resCache = new Dictionary<String, ResourceLocator>(FastResourceComparer.Default);
|
|
_defaultReader = new ResourceReader(stream, _resCache);
|
|
Reader = _defaultReader;
|
|
}
|
|
#endif // LOOSELY_LINKED_RESOURCE_REFERENCE
|
|
|
|
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, Environment.GetResourceString("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("key");
|
|
if (Reader == null || _resCache == null)
|
|
throw new ObjectDisposedException(null, Environment.GetResourceString("ObjectDisposed_ResourceSet"));
|
|
Contract.EndContractBlock();
|
|
|
|
Object value = null;
|
|
ResourceLocator resLocation;
|
|
|
|
lock(Reader) {
|
|
if (Reader == null)
|
|
throw new ObjectDisposedException(null, Environment.GetResourceString("ObjectDisposed_ResourceSet"));
|
|
|
|
if (_defaultReader != null) {
|
|
BCLDebug.Log("RESMGRFILEFORMAT", "Going down fast path in RuntimeResourceSet::GetObject");
|
|
|
|
// 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) {
|
|
Contract.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) {
|
|
#if LOOSELY_LINKED_RESOURCE_REFERENCE
|
|
if (Assembly != null && (value is LooselyLinkedResourceReference)) {
|
|
LooselyLinkedResourceReference assRef = (LooselyLinkedResourceReference) value;
|
|
value = assRef.Resolve(Assembly);
|
|
}
|
|
#endif // LOOSELY_LINKED_RESOURCE_REFERENCE
|
|
|
|
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 _DEBUG
|
|
BCLDebug.Perf(!ignoreCase, "Using case-insensitive lookups is bad perf-wise. Consider capitalizing "+key+" correctly in your source");
|
|
#endif
|
|
|
|
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 {
|
|
Contract.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;
|
|
}
|
|
}
|
|
#if LOOSELY_LINKED_RESOURCE_REFERENCE
|
|
if (Assembly != null && value is LooselyLinkedResourceReference) {
|
|
LooselyLinkedResourceReference assRef = (LooselyLinkedResourceReference) value;
|
|
value = assRef.Resolve(Assembly);
|
|
}
|
|
#endif // LOOSELY_LINKED_RESOURCE_REFERENCE
|
|
return value;
|
|
}
|
|
}
|
|
}
|