Managed Resources¶
Note
The documentation has a new home: Check it out!
.NET modules may define one or more resource files. Similar to Win32 resources, these are files that contain additional data, such as images, strings or audio files, that are used by the module at run time.
Manifest Resources¶
AsmResolver models managed resources using the ManifestResource
class, and they are exposed by the ModuleDefinition.Resources
property. Below an example snippet that prints the names of all resources in a given module:
var module = ModuleDefinition.FromFile(...);
foreach (var resource in module.Resources)
Console.WriteLine(resource.Name);
A ManifestResource
can either be embedded in the module itself, or present in an external assembly or file. When it is embedded, the contents of the file can be accessed using the EmbeddedDataSegment
property. This is a mutable property, so it is also possible to assign new data to the resource this way.
ManifestResource resource = ...
if (resource.IsEmbedded)
{
// Get data segment of the resource.
var oldData = resource.EmbeddedDataSegment;
// Assign new data to the resource.
var newData = new DataSegment(new byte[] { 1, 2, 3, 4});
resource.EmbeddedDataSegment = newData;
}
The ManifestResource
class also defines a convenience GetData
method, for quickly obtaining the data stored in the resource as a byte[]
:
ManifestResource resource = ...
if (resource.IsEmbedded)
{
byte[] data = resource.GetData();
// ...
}
Alternatively, you can use the TryGetReader
method to immediately instantiate a BinaryStreamReader
for the data. This can be useful if you want to parse the contents of the resource file later.
ManifestResource resource = ...
if (resource.TryGetReader(out var reader)
{
// ...
}
If the resource is not embedded, the Implementation
property will indicate in which file the resource can be found, and Offset
will indicate where in this file the data starts.
ManifestResource resource = ...
switch (resource.Implementation)
{
case FileReference fileRef:
// Resource is stored in another file.
string name = fileRef.Name;
uint offset = resource.Offset;
...
break;
case AssemblyReference assemblyRef:
// Resource is stored in another assembly.
var assembly = assemblyRef.Resolve();
var actualResource = assembly.ManifestModule.Resources.First(r => r.Name == resource.Name);
...
break;
case null:
// Resource is embedded.
...
break
}
Resource Sets¶
Many .NET applications (mainly Windows Forms apps) make use of manifest resources to store resource sets. These are resources that have the .resources
file extension, and combine multiple smaller resources (often localized strings or images) into one manifest resource file.
AsmResolver supports parsing and building new resource sets using the ResourceSet
class. This class is defined in the AsmResolver.DotNet.Resources
namespace:
using AsmResolver.DotNet.Resources;
Warning
Adding this using
statement might introduce a name resolution conflict with the (original) ResourceSet
class defined in System.Resources
. Generally speaking, you will not need both classes at the same time, as ResourceSet
from AsmResolver is meant to replace the one from System.Resources
. However, if you do need to use both classes in the same file, make sure you are using the correct one for your use-case. This can for example be achieved by specifying the fully qualified name (e.g. System.Resources.ResourceSet
), or by introducing an alias (e.g. using SystemResourceSet = System.Resources.ResourceSet;
) instead.
Creating new Resource Sets¶
Creating new sets can be done using the constructors of ResourceSet
.
var set = new ResourceSet();
By default, the parameterless constructor will create a resource set with a header that references the System.Resources.ResourceReader
and System.Resources.RuntimeResourceSet
types, both from mscorlib
version 4.0.0.0
. This can be customized if needed, by using another constructor overload that takes a ResourceManagerHeader
instance instead:
var set = new ResourceSet(ResourceManagerHeader.Deserializing_v4_0_0_0);
Alternatively, you can change the header using the ResourceSet.ManagerHeader
property:
var set = new ResourceSet();
set.ManagerHeader = ResourceManagerHeader.Deserializing_v4_0_0_0;
Reading existing Resource Sets¶
Reading existing resource sets can be done using the ResourceSet.FromReader
method:
ManifestResource resource = ...
if (resource.TryGetReader(out var reader)
{
var set = ResourceSet.FromReader(reader);
// ...
}
By default, AsmResolver will read and deserialize entries in a resource set. However, to prevent arbitrary code execution, it will not interpret the data of each entry that is of a non-intrinsic resource type. For these types of entries, AsmResolver will expose the raw data as a byte[]
instead. If you want to change this behavior, you can provide a custom instance of IResourceDataSerializer
or extend the default serializer so that it supports additional resource types.
public class MyResourceDataSerializer : DefaultResourceDataSerializer
{
/// <inheritdoc />
public override object? Deserialize(ref BinaryStreamReader reader, ResourceType type)
{
// ...
}
}
ManifestResource resource = ...
if (resource.TryGetReader(out var reader)
{
var set = ResourceSet.FromReader(reader, new MyResourceDataSerializer());
// ...
}
Accessing Resource Set Entries¶
The ResourceSet
class is a mutable list of ResourceSetEntry
, which includes the name, the type of the resource and the deserialized data:
foreach (var entry in set)
{
Console.WriteLine("Name: " + entry.Name);
Console.WriteLine("Type: " + entry.Type.FullName);
Console.WriteLine("Data: " + entry.Data);
}
New items can be created using any of the constructors.
var stringEntry = new ResourceSetEntry("MyString", ResourceTypeCode.String, "Hello, world!");
set.Add(stringEntry);
var intEntry = new ResourceSetEntry("MyInt", ResourceTypeCode.Int32, 1234);
set.Add(intEntry);
AsmResolver also supports reading and adding resource elements that are of a user-defined type:
var pointType = new UserDefinedResourceType(
"System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a");
var serializedContents = new byte[]
{
0x03, 0x06, 0x31, 0x32, 0x2C, 0x20, 0x33, 0x34 // "12, 34"
};
var entry = new ResourceSetEntry("MyLocation", type, serializedContents);
set.Add(entry);
Note
When using user-defined types, some implementations of the CLR will require a special resource reader type (such as System.Resources.Extensions.DeserializingResourceReader
) to be referenced in the manager header of the resource set. Therefore, make sure you have the right manager header provided in the ResourceSet
that defines such a compatible reader type.
Writing Resource Sets¶
Serializing resource sets can be done using the ResourceSet.Write
method.
using var stream = new MemoryStream();
var writer = new BinaryStreamWriter(stream);
set.Write(writer);
By default, AsmResolver will serialize entries in a resource set using a default serializer. However, to prevent arbitrary code execution, it will not attempt to serialize objects that are of a non-intrinsic resource type. The default serializer expects a byte[]
for user-defined resource types. If you want to change this behavior, you can provide a custom instance of IResourceDataSerializer
or extend the default serializer so that it supports additional resource types.
public class MyResourceDataSerializer : DefaultResourceDataSerializer
{
/// <inheritdoc />
public override void Serialize(IBinaryStreamWriter writer, ResourceType type, object? value)
{
// ...
}
}
using var stream = new MemoryStream();
var writer = new BinaryStreamWriter(stream);
set.Write(writer, new MyResourceDataSerializer());