diff --git a/docs/guides/peimage/win32resources.md b/docs/guides/peimage/win32resources.md index 8f9387519..648c72fc7 100644 --- a/docs/guides/peimage/win32resources.md +++ b/docs/guides/peimage/win32resources.md @@ -90,6 +90,10 @@ if (dataEntry.CanRead) } ``` +The data read using `CreateReader` is raw data as it appears in the file. +AsmResolver provides rich interpretations for some of the resource types. +See the [AsmResolver.PE.Win32Resources](../win32res/index.md) extension package for all supported resource types. + Adding new data entries can be done by using the `ResourceData` constructor: diff --git a/docs/guides/toc.yml b/docs/guides/toc.yml index 8cf8b1e53..3f8fe1cbc 100644 --- a/docs/guides/toc.yml +++ b/docs/guides/toc.yml @@ -78,6 +78,15 @@ - name: Advanced PE Image Building href: dotnet/advanced-pe-image-building.md +- name: Win32 Resources +- name: Overview + href: win32res/index.md +- name: Version Info + href: win32res/version.md +- name: Icons and Cursors + href: win32res/icons.md + + - name: PDB Symbols - name: Overview href: pdb/index.md diff --git a/docs/guides/win32res/icons.md b/docs/guides/win32res/icons.md new file mode 100644 index 000000000..dee2b92f7 --- /dev/null +++ b/docs/guides/win32res/icons.md @@ -0,0 +1,87 @@ +# Icons and Cursors (RT_GROUP_CURSOR, RT_GROUP_ICON, RT_CURSOR, RT_ICON) + +Icons and cursors follow a near-identical structure in the PE file format, and closely resemble the ICO file format. +Each icon is actually a set of individual icon files containing the same image in various resolutions, pixel formats and color palettes. + +The relevant code for reading and writing icon resources can be found in the following namespace: + +```csharp +using AsmResolver.PE.Win32Resources.Icon; +``` + +Icon resources are represented using the `IconResource` class. +In the following, basic usage of this class is described. + +## Creating Icon Resources + +To create a new icon resource, simply use the constructor of the `IconResource` class, specifying the type of icons to store. + +```csharp +var icons = new IconResource(IconType.Icon); +``` + +```csharp +var cursors = new IconResource(IconType.Cursor); +``` + + +## Reading Icon Resources + +To extract existing icons from a Portable Executable file, you will need to access the root resource directory of a `PEImage`. +Then, all icon groups can be obtained using the `FromDirectory` factory method: + +```csharp +PEImage image = ...; +var icons = IconResource.FromDirectory(image.Resources, IconType.Icon); +``` + + +## Icon Groups + +Icon groups are exposed using the `Groups` property, and can be iterated and modified: + +```csharp +foreach (var iconGroup in icons.Groups) +{ + Console.WriteLine($"ID: {iconGroup.Id}, LCID: {iconGroup.Lcid}"); +} +``` + +Each icon group consists of a set of `Icons`, each containing pixel data for a specific icon of a particular resolution and format: + +```csharp +IconGroup iconGroup = ...; +foreach (var icon in iconGroup.Icons) +{ + Console.WriteLine($"- {icon.Id}, {icon.Width}x{icon.Height}, {icon.PixelData.GetPhysicalSize()} bytes"); +} +``` + +A single `IconEntry` contains both fields for the ICO or CUR header such as `Width` and `Height`, as well as the raw pixel data in the `PixelData` property. +AsmResolver does not provide a way to interpret or reconstruct pixel data stored in an icon entry. +It only exposes a raw `ISegment` which can be read using a `BinaryStreamReader` or turned into a byte array (see [Reading Segment Contents](../core/segments.md#reading-segment-contents)). + +```csharp +byte[] rawData = icon.PixelData.WriteIntoArray(); +``` + +> [!NOTE] +> The `PixelData` property contains the icon's raw data **excluding** the ICO or CUR header. +> When modifying icon entries, make sure the header fields in the `IconEntry` object itself are therefore updated accordingly. + + +All collections and properties are mutable, and as such can be used to modify icons stored in a PE file. + + +## Writing Icon Resources + +To serialize the (modified) icon resource back to the resources of a PE image, use the `InsertIntoDirectory` method: + +```csharp +PEImage image = ...; +IconResource icons = ...; +icons.InsertIntoDirectory(image.Resources); +``` + +The PE image can then be saved as normal. +Either rebuild the PE image (see [Writing a PE Image](../peimage/basics.md#writing-a-pe-image)), or simply add a new section to the PE file with the contents of `image.Resources` (see [Adding a new Section](../pefile/sections.md#adding-a-new-section)). \ No newline at end of file diff --git a/docs/guides/win32res/index.md b/docs/guides/win32res/index.md new file mode 100644 index 000000000..1ba4f19c9 --- /dev/null +++ b/docs/guides/win32res/index.md @@ -0,0 +1,14 @@ +# Overview + +Win32 resources are additional files embedded into the PE image, and are organized by their resource type in directories typically stored in the `.rsrc` section. + +While `AsmResolver.PE` provides basic traversal of these win32 resource directories (see [Win32 Resources](../peimage/win32resources.md)), it stops at exposing the raw data of each of the embedded files. +The `AsmResolver.PE.Win32Resources` package is an extension that provides a richer API for reading and writing resource data for various resource types. + +The following resource types are supported by this extension package: + +- [RT_CURSOR](icons.md) +- [RT_GROUP_CURSOR](icons.md) +- [RT_GROUP_ICON](icons.md) +- [RT_ICON](icons.md) +- [RT_VERSIONINFO](version.md) \ No newline at end of file diff --git a/docs/guides/win32res/version.md b/docs/guides/win32res/version.md new file mode 100644 index 000000000..a9b9c350e --- /dev/null +++ b/docs/guides/win32res/version.md @@ -0,0 +1,125 @@ +# Version Info (RT_VERSIONINFO) + +The `RT_VERSIONINFO` resource type stores metadata describing product names, version numbers and copyright holders that are associated to a Portable Executable (PE) file. + +The relevant code for reading and writing version information can be found in the following namespace: + +```csharp +using AsmResolver.PE.Win32Resources.Version; +``` + +AsmResolver represents version metadata using the `VersionInfoResource` class. +In the following, basic usage of this class is described. + + +## Creating Version Info + +Creating a new version info resource can be done using the constructor of `VersionInfoResource`: + +```csharp +var versionInfo = new VersionInfoResource(); +``` + +By default, this creates a language-neutral version info (LCID: 0). To customize this, use the constructor overload: + +```csharp +var versionInfo = new VersionInfoResource(lcid: 1033); +``` + + +## Reading Version Info + +To extract existing version information from a Portable Executable file, you will need to access the root resource directory of a `PEImage`. +Then, the main version info directory can be obtained using the `FromDirectory` factory method: + +```csharp +PEImage image = ...; +var versionInfo = VersionInfoResource.FromDirectory(image.Resources); +``` + +If the executable contains multiple version info entries for different languages, use the `FindAllFromDirectory` method instead: + +```csharp +PEImage image = ...; +var versionInfos = VersionInfoResource.FindAllFromDirectory(image.Resources); +``` + +To retrieve version info with a specific language identifier (LCID), use the `FromDirectory` overload accepting an extra parameter: + +```csharp +PEImage image = ...; +var versionInfo = VersionInfoResource.FromDirectory(image.Resources, lcid: 1033); +``` + +## Fixed File Version Info + +Every version info resource starts with a fixed file version info header, exposed via the `FixedVersionInfo` property. + +```csharp +Console.WriteLine($"File Version: {versionInfo.FixedVersionInfo.FileVersion}"); +Console.WriteLine($"Product Version: {versionInfo.FixedVersionInfo.ProductVersion}"); +Console.WriteLine($"Target OS: {versionInfo.FixedVersionInfo.FileOS}"); +``` + +All properties in this object are mutable, and thus can be modified to change the version info of the PE file. + + +## String and Var Tables + +Version info metadata may also contain additional string tables that can contain arbitrary strings. +The way they are organized in optional blobs after the fixed version info, in good PE file format fashion with a good amount of redundancy. + +String tables are stored in `StringFileInfo` instances, which can be created or extracted as follows: + +```csharp +var stringInfo = new StringFileInfo(); +versionInfo.AddEntry(stringInfo); +``` + +```csharp +var stringInfo = versionInfo.GetChild(StringFileInfo.StringFileInfoKey); +``` + +A single string table can then be created as follows: + +```csharp +var stringTable = new StringTable(languageIdentifier: 1033, codePage: 1200); +stringInfo.Tables.Add(stringTable); +``` +```csharp +var stringTable = stringInfo.Tables[0]; +``` + +String tables contain information such as product name and copyright information: + +```csharp +Console.WriteLine($"Product Name: {stringTable[StringTable.ProductNameKey]}"); +Console.WriteLine($"Copyright: {stringTable[StringTable.LegalCopyrightKey]}"); +``` + +Each string table is accompanied with a `VarTable`, stored in the `VarFileInfo` blob, containing both the language identifier and code page: + +```csharp +var varInfo = new VarFileInfo(); +versionInfo.AddEntry(varTable); +``` + +```csharp +var varTable = new VarTable(); +varTable.Values.Add(1033u | 1200u << 16); +varInfo.Tables.Add(varTable); +``` + + +## Writing Version Info + +To serialize the (modified) version info metadata back to the resources of a PE image, use the `InsertIntoDirectory` method: + +```csharp +PEImage image = ...; +VersionInfoResource versionInfo = ...; +versionInfo.InsertIntoDirectory(image.Resources); +``` + +The PE image can then be saved as normal. +Either rebuild the PE image (see [Writing a PE Image](../peimage/basics.md#writing-a-pe-image)), or simply add a new section to the PE file with the contents of `image.Resources` (see [Adding a new Section](../pefile/sections.md#adding-a-new-section)). \ No newline at end of file diff --git a/src/AsmResolver.PE.Win32Resources/AsmResolver.PE.Win32Resources.csproj b/src/AsmResolver.PE.Win32Resources/AsmResolver.PE.Win32Resources.csproj index 98bce0723..6af1f8737 100644 --- a/src/AsmResolver.PE.Win32Resources/AsmResolver.PE.Win32Resources.csproj +++ b/src/AsmResolver.PE.Win32Resources/AsmResolver.PE.Win32Resources.csproj @@ -5,6 +5,7 @@ exe pe directories imports exports resources dotnet cil inspection manipulation assembly disassembly 1701;1702;NU5105 true + 12 @@ -20,6 +21,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/AsmResolver.PE.Win32Resources/Icon/IconEntry.cs b/src/AsmResolver.PE.Win32Resources/Icon/IconEntry.cs index 3fd3a91e2..f7b38b2dd 100644 --- a/src/AsmResolver.PE.Win32Resources/Icon/IconEntry.cs +++ b/src/AsmResolver.PE.Win32Resources/Icon/IconEntry.cs @@ -1,48 +1,156 @@ -using System; +using System; using AsmResolver.IO; -using AsmResolver.Shims; -namespace AsmResolver.PE.Win32Resources.Icon +namespace AsmResolver.PE.Win32Resources.Icon; + +/// +/// Represents a single icon entry in an icon group. +/// +public class IconEntry { + private readonly LazyVariable _pixelData; + + /// + /// Initializes an empty icon entry. + /// + protected IconEntry() + { + _pixelData = new LazyVariable(x => x.GetData()); + } + + /// + /// Creates a new icon entry with the provided identifier and language specifier. + /// + /// The identifier of the icon. + /// The language identifier. + public IconEntry(ushort id, uint lcid) + : this() + { + Id = id; + Lcid = lcid; + } + + /// + /// Gets or sets the identifier of the icon. + /// + public ushort Id + { + get; + set; + } + /// - /// Represents a single icon resource entry. - /// - public class IconEntry : SegmentBase - { - /// - /// The raw bytes of the icon. - /// - public byte[] RawIcon - { - get; - set; - } = ArrayShim.Empty(); - - /// - /// Reads an icon resource entry from an input stream. - /// - /// The input stream. - /// The parsed icon resource entry. - public static IconEntry FromReader(ref BinaryStreamReader reader) - { - var result = new IconEntry - { - Offset = reader.Offset, - Rva = reader.Rva, - RawIcon = new byte[reader.Length] - }; - reader.ReadBytes(result.RawIcon, 0, (int)reader.Length); - - return result; - } - - /// - public override uint GetPhysicalSize() => (uint)RawIcon.Length; - - /// - public override void Write(BinaryStreamWriter writer) - { - writer.WriteBytes(RawIcon, 0, RawIcon.Length); - } + /// Gets or sets the language identifier of the icon. + /// + public uint Lcid + { + get; + set; + } + + /// + /// Gets or sets the width of the icon in pixels. A width of 0 indicates 256 pixels. + /// + public byte Width + { + get; + set; + } + + /// + /// Gets or sets the height of the icon in pixels. A width of 0 indicates 256 pixels. + /// + public byte Height + { + get; + set; + } + + /// + /// Gets or sets the number of colors in the color palette. Should be 0 if the image does not use a color palette. + /// + public byte ColorCount + { + get; + set; + } + + /// + /// Reserved, should be zero. + /// + public byte Reserved + { + get; + set; + } + + /// + /// When the icon is in ICO format: Gets or sets the number of color planes. Should be 0 or 1. + /// When the icon is in CUR format: Gets or sets the horizontal coordinates of the hotspot in number of pixels from + /// the left. + /// + public ushort Planes + { + get; + set; + } + + /// + /// When the icon is in ICO format: Gets or sets the number of bits per pixel. + /// When the icon is in CUR format: Gets or sets the vertical coordinates of the hotspot in number of pixels from + /// the top. + /// + public ushort BitsPerPixel + { + get; + set; + } + + /// + /// Gets or sets the raw pixel data of the icon. + /// + public ISegment? PixelData + { + get => _pixelData.GetValue(this); + set => _pixelData.SetValue(value); + } + + /// + /// Obtains the raw pixel data of the icon. + /// + /// The data. + /// + /// This method is called upon initialization of the property. + /// + protected virtual ISegment? GetData() => null; + + /// + /// Serializes the icon entry to the provided output stream and inserts the icon pixel data into the provided + /// icons resource directory. + /// + /// The icon entry directory to insert into. + /// The output stream. + /// Occurs when there is already an icon added with the same ID. + /// Occurs when is nulL. + public void Write(ResourceDirectory entryDirectory, BinaryStreamWriter writer) + { + writer.WriteByte(Width); + writer.WriteByte(Height); + writer.WriteByte(ColorCount); + writer.WriteByte(Reserved); + writer.WriteUInt16(Planes); + writer.WriteUInt16(BitsPerPixel); + writer.WriteUInt32(PixelData?.GetPhysicalSize() ?? 0); + writer.WriteUInt16(Id); + + if (entryDirectory.TryGetEntry(Id, out _)) + throw new ArgumentException($"Duplicate icon resource with ID {Id}."); + + if (PixelData is null) + throw new ArgumentNullException($"Icon resource ID {Id} has no pixel data."); + + var entry = new ResourceDirectory(Id); + entry.Entries.Add(new ResourceData(Lcid, PixelData)); + entryDirectory.InsertOrReplaceEntry(entry); } } diff --git a/src/AsmResolver.PE.Win32Resources/Icon/IconGroup.cs b/src/AsmResolver.PE.Win32Resources/Icon/IconGroup.cs new file mode 100644 index 000000000..2d6ffd8bc --- /dev/null +++ b/src/AsmResolver.PE.Win32Resources/Icon/IconGroup.cs @@ -0,0 +1,119 @@ +using System.Collections.Generic; +using System.Threading; +using AsmResolver.IO; + +namespace AsmResolver.PE.Win32Resources.Icon; + +/// +/// Represents a single icon group. +/// +public class IconGroup +{ + private IList? _icons; + + /// + /// Creates a new icon group. + /// + /// The identifier of the icon group. + /// The language specifier of the icon group. + public IconGroup(uint id, uint lcid) + { + Id = id; + Lcid = lcid; + } + + /// + /// Creates a new icon group. + /// + /// The identifier of the icon group. + /// The language specifier of the icon group. + public IconGroup(string name, uint lcid) + { + Name = name; + Lcid = lcid; + } + + /// + /// Gets or sets the identifier of the icon. + /// + /// + /// This value has no meaning if is not null. + /// + public uint Id + { + get; + set; + } + + /// + /// Gets or sets the name of the icon. When null, is used instead. + /// + public string? Name + { + get; + set; + } + + /// + /// Gets or sets the language specifier of the group. + /// + public uint Lcid + { + get; + } + + /// + /// Reserved, should be zero. + /// + public ushort Reserved + { + get; + set; + } + + /// + /// Gets or sets the type of icon that is stored in the group. + /// + public IconType Type + { + get; + set; + } + + /// + /// Gets the icons stored in the group. + /// + public IList Icons + { + get + { + if (_icons is null) + Interlocked.CompareExchange(ref _icons, GetIcons(), null); + return _icons; + } + } + + /// + /// Obtains the icons stored in the group. + /// + /// The icons. + /// + /// This method is called upon initialization of the property. + /// + protected virtual IList GetIcons() => new List(); + + /// + /// Serializes the icon group and inserts all icons in the group into the provided resource directory. + /// + /// The icon entry directory. + /// The output stream. + public void Write(ResourceDirectory entryDirectory, BinaryStreamWriter writer) + { + writer.WriteUInt16(Reserved); + writer.WriteUInt16((ushort) Type); + writer.WriteUInt16((ushort) Icons.Count); + + for (int i = 0; i < Icons.Count; i++) + Icons[i].Write(entryDirectory, writer); + } +} diff --git a/src/AsmResolver.PE.Win32Resources/Icon/IconGroupDirectory.cs b/src/AsmResolver.PE.Win32Resources/Icon/IconGroupDirectory.cs deleted file mode 100644 index 8e3fdf611..000000000 --- a/src/AsmResolver.PE.Win32Resources/Icon/IconGroupDirectory.cs +++ /dev/null @@ -1,152 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using AsmResolver.IO; - -namespace AsmResolver.PE.Win32Resources.Icon -{ - /// - /// Represents a single win32 icon group directory. - /// - public class IconGroupDirectory : SegmentBase - { - /// - /// Used to keep track of all icon entries associated with this icon group. - /// - private readonly Dictionary _entries = new(); - - /// - /// Reads a single icon group directory. - /// - /// The reader. - /// The icon resource directory used to extract associated icon entries from. - /// The icon group directory. - public static IconGroupDirectory FromReader(ref BinaryStreamReader reader, ResourceDirectory iconResourceDirectory) - { - var result = new IconGroupDirectory - { - Offset = reader.Offset, - Rva = reader.Rva, - Reserved = reader.ReadUInt16(), - Type = reader.ReadUInt16(), - Count = reader.ReadUInt16() - }; - - for (int i = 0; i < result.Count; i++) - { - var (iconGroupDirectoryEntry, iconEntry) = ReadNextEntry(ref reader, iconResourceDirectory); - result.AddEntry(iconGroupDirectoryEntry, iconEntry); - } - - return result; - } - - private static (IconGroupDirectoryEntry, IconEntry) ReadNextEntry(ref BinaryStreamReader reader, ResourceDirectory iconResourceDirectory) - { - var entry = IconGroupDirectoryEntry.FromReader(ref reader); - - // search for icon reference in icon resource directory - var iconDirectory = iconResourceDirectory - .Entries - .OfType() - .FirstOrDefault(d => d.Id == entry.Id); - - var iconDataEntry = iconDirectory? - .Entries - .OfType() - .FirstOrDefault(); - - if (iconDataEntry is null) - throw new ArgumentException("Non-existent icon reference."); - - var iconReader = iconDataEntry.CreateReader(); - var entry2 = IconEntry.FromReader(ref iconReader); - - return (entry, entry2); - } - - /// - /// Gets or sets an icon entry by its id. - /// - /// The id of the icon entry. - public (IconGroupDirectoryEntry, IconEntry) this[ushort id] - { - get => _entries[id]; - set => _entries[id] = value; - } - - /// - /// Adds or overrides the existing entry with the same id to the group icon resource. - /// - /// The icon group directory entry to add. - /// The icon entry to add. - public void AddEntry(IconGroupDirectoryEntry iconGroupDirectoryEntry, IconEntry iconEntry) => - _entries[iconGroupDirectoryEntry.Id] = (iconGroupDirectoryEntry, iconEntry); - - /// - /// Removes an existing entry with a specified id from the group icon resource. - /// - /// The group icon id. - /// True if the icon resource was successfully removed, otherwise false. - public bool RemoveEntry(ushort id) => _entries.Remove(id); - - /// - /// Gets a collection of icon entries stored in the icon group. - /// - public IEnumerable<(IconGroupDirectoryEntry, IconEntry)> GetIconEntries() => _entries.Values; - - /// - /// The size of the header in bytes. - /// - public const uint HeaderSize = sizeof(ushort) // Reserved - + sizeof(ushort) // Type - + sizeof(ushort); // Count - - /// - /// Reserved field. Must be 0. - /// - public ushort Reserved - { - get; - set; - } - - /// - /// Gets or sets the resource type. - /// - public ushort Type - { - get; - set; - } - - /// - /// Gets or sets the amount of entries in this resource directory. - /// - public ushort Count - { - get; - set; - } - - /// - public override uint GetPhysicalSize() - { - uint totalEntrySize = 0; - foreach (var entry in _entries) - totalEntrySize += entry.Value.Item1.GetPhysicalSize(); - - return HeaderSize + totalEntrySize; - } - - /// - public override void Write(BinaryStreamWriter writer) - { - writer.WriteUInt16(Reserved); - writer.WriteUInt16(Type); - writer.WriteUInt16(Count); - foreach (var entry in _entries) - entry.Value.Item1.Write(writer); - } - } -} diff --git a/src/AsmResolver.PE.Win32Resources/Icon/IconGroupDirectoryEntry.cs b/src/AsmResolver.PE.Win32Resources/Icon/IconGroupDirectoryEntry.cs deleted file mode 100644 index 43aee8adc..000000000 --- a/src/AsmResolver.PE.Win32Resources/Icon/IconGroupDirectoryEntry.cs +++ /dev/null @@ -1,131 +0,0 @@ -using AsmResolver.IO; - -namespace AsmResolver.PE.Win32Resources.Icon -{ - /// - /// Represents a single group icon resource entry. - /// - public class IconGroupDirectoryEntry : SegmentBase - { - /// - /// The id of the icon entry. - /// - public ushort Id - { - get; - set; - } - - /// - /// The width of the icon. - /// - public byte Width - { - get; - set; - } - - /// - /// The height of the icon. - /// - public byte Height - { - get; - set; - } - - /// - /// The amount of colors. - /// - public byte ColorCount - { - get; - set; - } - - /// - /// Reserved field. Must be 0. - /// - public byte Reserved - { - get; - set; - } - - /// - /// The amount of color planes. - /// - public ushort ColorPlanes - { - get; - set; - } - - /// - /// The amount of bits per pixel. - /// - public ushort PixelBitCount - { - get; - set; - } - - /// - /// The length of the icon. - /// - public uint BytesInRes - { - get; - set; - } - - /// - /// Reads an icon group resource entry from an input stream. - /// - /// The input stream. - /// The parsed group icon resource entry. - public static IconGroupDirectoryEntry FromReader(ref BinaryStreamReader reader) - { - return new IconGroupDirectoryEntry - { - Offset = reader.Offset, - Rva = reader.Rva, - Width = reader.ReadByte(), - Height = reader.ReadByte(), - ColorCount = reader.ReadByte(), - Reserved = reader.ReadByte(), - ColorPlanes = reader.ReadUInt16(), - PixelBitCount = reader.ReadUInt16(), - BytesInRes = reader.ReadUInt32(), - Id = reader.ReadUInt16() - }; - } - - /// - public override uint GetPhysicalSize() - { - return sizeof(byte) // Width - + sizeof(byte) // Height - + sizeof(byte) // ColorCount - + sizeof(byte) // Reserved - + sizeof(ushort) // ColorPlanes - + sizeof(ushort) // PixelBitCount - + sizeof(uint) // BytesInRes - + sizeof(ushort); // Id - - } - - /// - public override void Write(BinaryStreamWriter writer) - { - writer.WriteByte(Width); - writer.WriteByte((Height)); - writer.WriteByte(ColorCount); - writer.WriteByte(Reserved); - writer.WriteUInt16(ColorPlanes); - writer.WriteUInt16(PixelBitCount); - writer.WriteUInt32(BytesInRes); - writer.WriteUInt16(Id); - } - } -} diff --git a/src/AsmResolver.PE.Win32Resources/Icon/IconResource.cs b/src/AsmResolver.PE.Win32Resources/Icon/IconResource.cs index 077b15db6..3a6cdeb09 100644 --- a/src/AsmResolver.PE.Win32Resources/Icon/IconResource.cs +++ b/src/AsmResolver.PE.Win32Resources/Icon/IconResource.cs @@ -1,110 +1,261 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Linq; +using AsmResolver.IO; -namespace AsmResolver.PE.Win32Resources.Icon +namespace AsmResolver.PE.Win32Resources.Icon; + +/// +/// Provides a high level view on icon and cursor resources stored in a PE image. +/// +public class IconResource : IWin32Resource { /// - /// Represents a view on win32 icon group resource directories and includes access to their icon entries. + /// Creates a new icon resource of the provided icon type. + /// + /// The icon type. + public IconResource(IconType type) + { + Type = type; + } + + /// + /// Gets the icon type the resource is storing. + /// + public IconType Type + { + get; + } + + /// + /// Gets the icon groups stored in the resource. + /// + public IList Groups + { + get; + } = new List(); + + /// + /// Reads icons from the provided root resource directory of a PE image. /// - public class IconResource : IWin32Resource + /// The root resource directory. + /// The icon type. + /// The resource, or null if no resource of the provided type was present. + public static IconResource? FromDirectory(ResourceDirectory rootDirectory, IconType type) { - /// - /// Used to keep track of icon groups. - /// - private readonly Dictionary _entries = new(); - - /// - /// Obtains the icon group resources from the provided root win32 resources directory. - /// - /// The root resources directory to extract the icon group from. - /// The icon group resources, or null if none was found. - /// Occurs when the resource data is not readable. - public static IconResource? FromDirectory(ResourceDirectory rootDirectory) + var (groupType, entryType) = GetResourceDirectoryTypes(type); + + if (!rootDirectory.TryGetDirectory(groupType, out var groupDirectory)) + return null; + + if (!rootDirectory.TryGetDirectory(entryType, out var iconsDirectory)) + return null; + + var result = new IconResource(type); + + foreach (var group in groupDirectory.Entries.OfType()) { - if (!rootDirectory.TryGetDirectory(ResourceType.GroupIcon, out var groupIconDirectory) - || !rootDirectory.TryGetDirectory(ResourceType.Icon, out var iconDirectory)) + foreach (var language in group.Entries.OfType()) { - return null; + result.Groups.Add(new SerializedIconGroup( + group.Id, + language.Id, + iconsDirectory, + language.CreateReader() + )); } + } - var result = new IconResource(); + return result; + } - foreach (var iconGroupResource in groupIconDirectory.Entries.OfType()) - { - var dataEntry = iconGroupResource - .Entries - .OfType() - .FirstOrDefault(); + private static (ResourceType Group, ResourceType Entry) GetResourceDirectoryTypes(IconType type) + { + return type switch + { + IconType.Icon => (ResourceType.GroupIcon, ResourceType.Icon), + IconType.Cursor => (ResourceType.GroupCursor, ResourceType.Cursor), + _ => throw new ArgumentOutOfRangeException(nameof(type)), + }; + } + + /// + /// Gets an icon group by its numeric identifier. + /// + /// The identifier. + /// The group. + /// Occurs when the group was not present. + public IconGroup GetGroup(uint id) + { + return TryGetGroup(id, out var group) + ? group + : throw new ArgumentOutOfRangeException($"Icon group {id} does not exist."); + } - if (dataEntry is null) - return null; + /// + /// Gets an icon group by its string identifier. + /// + /// The identifier. + /// The group. + /// Occurs when the group was not present. + public IconGroup GetGroup(string name) + { + return TryGetGroup(name, out var group) + ? group + : throw new ArgumentOutOfRangeException($"Icon group '{name}' does not exist."); + } - if (!dataEntry.CanRead) - throw new ArgumentException("Icon group data is not readable."); + /// + /// Gets an icon group by its numeric identifier. + /// + /// The identifier. + /// The language identifier. + /// The group. + /// Occurs when the group was not present. + public IconGroup GetGroup(uint id, uint lcid) + { + return TryGetGroup(id, lcid, out var group) + ? group + : throw new ArgumentOutOfRangeException($"Icon group {id} (language: {lcid}) does not exist."); + } - var groupReader = dataEntry.CreateReader(); - result.AddEntry(iconGroupResource.Id, IconGroupDirectory.FromReader(ref groupReader, iconDirectory)); + /// + /// Gets an icon group by its string identifier. + /// + /// The identifier. + /// The language identifier. + /// The group. + /// Occurs when the group was not present. + public IconGroup GetGroup(string name, uint lcid) + { + return TryGetGroup(name, lcid, out var group) + ? group + : throw new ArgumentOutOfRangeException($"Icon group {name} (language: {lcid}) does not exist."); + } + + /// + /// Attempts to get an icon group by its numeric identifier. + /// + /// The identifier. + /// The group, or null if none was found. + /// true if the group was found, false otherwise. + public bool TryGetGroup(uint id, [NotNullWhen(true)] out IconGroup? group) + { + foreach (var g in Groups) + { + if (g.Id == id) + { + group = g; + return true; } + } + + group = null; + return false; + } - return result; + /// + /// Attempts to get an icon group by its numeric identifier and language specifier. + /// + /// The identifier. + /// The language identifier. + /// The group, or null if none was found. + /// true if the group was found, false otherwise. + public bool TryGetGroup(uint id, uint lcid, [NotNullWhen(true)] out IconGroup? group) + { + foreach (var g in Groups) + { + if (g.Id == id && g.Lcid == lcid) + { + group = g; + return true; + } } - /// - /// Gets or sets an icon group by its id. - /// - /// The id of the icon group. - public IconGroupDirectory this[uint id] + group = null; + return false; + } + + /// + /// Attempts to get an icon group by its string identifier. + /// + /// The identifier. + /// The group, or null if none was found. + /// true if the group was found, false otherwise. + public bool TryGetGroup(string name, [NotNullWhen(true)] out IconGroup? group) + { + foreach (var g in Groups) { - get => _entries[id]; - set => _entries[id] = value; + if (g.Name == name) + { + group = g; + return true; + } } - /// - /// Adds or overrides the existing entry with the same id to the icon group resource. - /// - /// The id to use for the entry. - /// The entry to add. - public void AddEntry(uint id, IconGroupDirectory entry) => _entries[id] = entry; - - /// - /// Removes an existing entry with a specified id from the icon group resource. - /// - /// The icon group id. - /// True if the icon entry was successfully removed, otherwise false. - public bool RemoveEntry(uint id) => _entries.Remove(id); - - /// - /// Gets a collection of entries stored in the icon group directory. - /// - /// The collection of icon group entries. - public IEnumerable GetIconGroups() => _entries.Values; - - /// - public void InsertIntoDirectory(ResourceDirectory rootDirectory) + group = null; + return false; + } + + /// + /// Attempts to get an icon group by its string identifier and language specifier. + /// + /// The identifier. + /// The language identifier. + /// The group, or null if none was found. + /// true if the group was found, false otherwise. + public bool TryGetGroup(string name, uint lcid, [NotNullWhen(true)] out IconGroup? group) + { + foreach (var g in Groups) { - // Construct new directory. - var newGroupIconDirectory = new ResourceDirectory(ResourceType.GroupIcon); - foreach (var entry in _entries) + if (g.Name == name && g.Lcid == lcid) { - newGroupIconDirectory.InsertOrReplaceEntry(new ResourceDirectory(entry.Key) - {Entries = {new ResourceData(0u, entry.Value)}}); + group = g; + return true; } + } - // Construct new directory. - var newIconDirectory = new ResourceDirectory(ResourceType.Icon); - foreach (var entry in _entries) + group = null; + return false; + } + + /// + public void InsertIntoDirectory(ResourceDirectory rootDirectory) + { + var (groupType, entryType) = GetResourceDirectoryTypes(Type); + + // We're always replacing the existing directories with new ones. + var groupDirectory = new ResourceDirectory(groupType); + var entryDirectory = new ResourceDirectory(entryType); + + var groupDirectories = new Dictionary(); + foreach (var group in Groups) + { + // Create the group dir if it doesn't exist. + object groupId = (object?) group.Name ?? group.Id; + if (!groupDirectories.TryGetValue(groupId, out var directory)) { - foreach (var (groupEntry, iconEntry) in entry.Value.GetIconEntries()) - { - newIconDirectory.InsertOrReplaceEntry(new ResourceDirectory(groupEntry.Id) - {Entries = {new ResourceData(1033, iconEntry)}}); - } + directory = group.Name is not null + ? new ResourceDirectory(group.Name) + : new ResourceDirectory(group.Id); + + groupDirectories.Add(groupId, directory); + groupDirectory.Entries.Add(directory); } - // Insert. - rootDirectory.InsertOrReplaceEntry(newGroupIconDirectory); - rootDirectory.InsertOrReplaceEntry(newIconDirectory); + // Serialize the group. + using var stream = new MemoryStream(); + var writer = new BinaryStreamWriter(stream); + group.Write(entryDirectory, writer); + + // Add the group. + directory.Entries.Add(new ResourceData(group.Lcid, new DataSegment(stream.ToArray()))); } + + // Insert into the root win32 dir of the PE image. + rootDirectory.InsertOrReplaceEntry(groupDirectory); + rootDirectory.InsertOrReplaceEntry(entryDirectory); } } diff --git a/src/AsmResolver.PE.Win32Resources/Icon/IconType.cs b/src/AsmResolver.PE.Win32Resources/Icon/IconType.cs new file mode 100644 index 000000000..81c29306f --- /dev/null +++ b/src/AsmResolver.PE.Win32Resources/Icon/IconType.cs @@ -0,0 +1,17 @@ +namespace AsmResolver.PE.Win32Resources.Icon; + +/// +/// Provides members describing all possible icon resources that can be stored in a PE image. +/// +public enum IconType : ushort +{ + /// + /// Indicates the icons stored in the group are ICO files. + /// + Icon = 1, + + /// + /// Indicates the icons stored in the group are CUR files. + /// + Cursor = 2, +} diff --git a/src/AsmResolver.PE.Win32Resources/Icon/SerializedIconEntry.cs b/src/AsmResolver.PE.Win32Resources/Icon/SerializedIconEntry.cs new file mode 100644 index 000000000..bb3783a78 --- /dev/null +++ b/src/AsmResolver.PE.Win32Resources/Icon/SerializedIconEntry.cs @@ -0,0 +1,46 @@ +using AsmResolver.IO; + +namespace AsmResolver.PE.Win32Resources.Icon; + +/// +/// Provides a lazy-initialized implementation of that reads from an existing resource. +/// +public class SerializedIconEntry : IconEntry +{ + private readonly ResourceDirectory _iconsDirectory; + private readonly ushort _originalId; + private readonly uint _originalLcid; + + /// + /// Reads an icon entry from the provided input stream and corresponding icons directory. + /// + /// The language specifier. + /// The icons directory. + /// The input stream. + public SerializedIconEntry(uint lcid, ResourceDirectory iconsDirectory, ref BinaryStreamReader reader) + { + _iconsDirectory = iconsDirectory; + + Width = reader.ReadByte(); + Height = reader.ReadByte(); + ColorCount = reader.ReadByte(); + Reserved = reader.ReadByte(); + Planes = reader.ReadUInt16(); + BitsPerPixel = reader.ReadUInt16(); + _ = reader.ReadUInt32(); // dwBytesInRes. + Id = _originalId = reader.ReadUInt16(); + Lcid = _originalLcid = lcid; + } + + /// + protected override ISegment? GetData() + { + if (!_iconsDirectory.TryGetDirectory(_originalId, out var directory)) + return null; + + if (!directory.TryGetData(_originalLcid, out var iconData)) + return null; + + return iconData.Contents; + } +} diff --git a/src/AsmResolver.PE.Win32Resources/Icon/SerializedIconGroup.cs b/src/AsmResolver.PE.Win32Resources/Icon/SerializedIconGroup.cs new file mode 100644 index 000000000..3114fafab --- /dev/null +++ b/src/AsmResolver.PE.Win32Resources/Icon/SerializedIconGroup.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using AsmResolver.IO; + +namespace AsmResolver.PE.Win32Resources.Icon; + +/// +/// Provides a lazy-initialized implementation of that reads its icons from an existing resource. +/// +public class SerializedIconGroup : IconGroup +{ + private readonly ResourceDirectory _iconDirectory; + private readonly ushort _count; + private readonly BinaryStreamReader _itemReader; + + /// + /// Reads an icon group from the provided icon directory and input stream. + /// + /// The group identifier. + /// The group language identifier. + /// The directory containing the individual icons. + /// The input stream. + public SerializedIconGroup(uint id, uint lcid, ResourceDirectory iconDirectory, BinaryStreamReader reader) + : base(id, lcid) + { + _iconDirectory = iconDirectory; + + Reserved = reader.ReadUInt16(); + Type = (IconType) reader.ReadUInt16(); + _count = reader.ReadUInt16(); + _itemReader = reader; + } + + /// + protected override IList GetIcons() + { + var result = new List(); + + var reader = _itemReader; + for (int i = 0; i < _count; i++) + result.Add(new SerializedIconEntry(Lcid, _iconDirectory, ref reader)); + + return result; + } +} diff --git a/src/AsmResolver.PE.Win32Resources/Version/StringTable.cs b/src/AsmResolver.PE.Win32Resources/Version/StringTable.cs index 1ac78299f..fff95ea05 100644 --- a/src/AsmResolver.PE.Win32Resources/Version/StringTable.cs +++ b/src/AsmResolver.PE.Win32Resources/Version/StringTable.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Text; using AsmResolver.IO; @@ -11,7 +12,7 @@ namespace AsmResolver.PE.Win32Resources.Version /// Represents the organization of data in a file-version resource. It contains language and code page formatting /// information for the strings specified by the Children member. A code page is an ordered character set. /// - public class StringTable : VersionTableEntry, IEnumerable> + public class StringTable : VersionTableEntry, IDictionary { /// /// The name of the string describing the comments assigned to the executable file. @@ -73,7 +74,7 @@ public class StringTable : VersionTableEntry, IEnumerable public const string SpecialBuildKey = "SpecialBuild"; - private readonly Dictionary _entries = new(); + private readonly IDictionary _entries = new Dictionary(); /// /// Creates a new string table. @@ -110,16 +111,24 @@ public ushort CodePage /// protected override VersionTableValueType ValueType => VersionTableValueType.Binary; - /// - /// Gets or sets the value of a single field in the string table. - /// - /// The name of the field in the string table. + /// public string this[string key] { get => _entries[key]; set => _entries[key] = value; } + /// + public int Count => _entries.Count; + + bool ICollection>.IsReadOnly => false; + + /// + public ICollection Keys => _entries.Keys; + + /// + public ICollection Values => _entries.Values; + /// /// Reads a single StringTable structure from the provided input stream. /// @@ -169,21 +178,38 @@ private static KeyValuePair ReadEntry(ref BinaryStreamReader rea return new KeyValuePair(header.Key, value); } - /// - /// Adds (or overrides) a field to the string table. - /// - /// The name of the field. - /// The value of the field. - public void Add(string key, string value) => - _entries[key] = value ?? throw new ArgumentNullException(nameof(value)); + /// + public void Add(string key, string value) => _entries.Add(key, value); - /// - /// Removes a single field from the string table by its name. - /// - /// The name of the field. - /// true if the field existed and was removed successfully, false otherwise. + /// + public bool ContainsKey(string key) => _entries.ContainsKey(key); + + /// + public bool TryGetValue(string key, [NotNullWhen(true)] out string? value) => _entries.TryGetValue(key, out value); + + /// + public void Add(KeyValuePair item) => _entries.Add(item); + + /// public bool Remove(string key) => _entries.Remove(key); + /// + public bool Remove(KeyValuePair item) => _entries.Remove(item); + + /// + public void Clear() => _entries.Clear(); + + /// + public bool Contains(KeyValuePair item) => _entries.Contains(item); + + /// + public void CopyTo(KeyValuePair[] array, int arrayIndex) => _entries.CopyTo(array, arrayIndex); + + /// + public IEnumerator> GetEnumerator() => _entries.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + /// public override uint GetPhysicalSize() { @@ -229,7 +255,7 @@ private static void WriteEntry(BinaryStreamWriter writer, KeyValuePair - public IEnumerator> GetEnumerator() => _entries.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } } diff --git a/test/AsmResolver.PE.Win32Resources.Tests/AsmResolver.PE.Win32Resources.Tests.csproj b/test/AsmResolver.PE.Win32Resources.Tests/AsmResolver.PE.Win32Resources.Tests.csproj index 9cd8c9b8c..c664e53e6 100644 --- a/test/AsmResolver.PE.Win32Resources.Tests/AsmResolver.PE.Win32Resources.Tests.csproj +++ b/test/AsmResolver.PE.Win32Resources.Tests/AsmResolver.PE.Win32Resources.Tests.csproj @@ -5,6 +5,7 @@ false AsmResolver.PE.Win32Resources.Tests warnings + 12 diff --git a/test/AsmResolver.PE.Win32Resources.Tests/Icon/IconResourceTest.cs b/test/AsmResolver.PE.Win32Resources.Tests/Icon/IconResourceTest.cs index 10cdf60b8..c1fc6a6f6 100644 --- a/test/AsmResolver.PE.Win32Resources.Tests/Icon/IconResourceTest.cs +++ b/test/AsmResolver.PE.Win32Resources.Tests/Icon/IconResourceTest.cs @@ -1,58 +1,116 @@ -using System.IO; -using System.Linq; -using AsmResolver.IO; +using System.Linq; using AsmResolver.PE.Builder; using AsmResolver.PE.Win32Resources.Icon; +using AsmResolver.Tests.Runners; using Xunit; -namespace AsmResolver.PE.Win32Resources.Tests.Icon +namespace AsmResolver.PE.Win32Resources.Tests.Icon; + +public class IconResourceTest : IClassFixture { - public class IconResourceTest + private readonly TemporaryDirectoryFixture _fixture; + + public IconResourceTest(TemporaryDirectoryFixture fixture) { - [Fact] - public void ReadIconGroupResourceDirectory() - { - // Load dummy. - var image = PEImage.FromBytes(Properties.Resources.HelloWorld); + _fixture = fixture; + } - // Read icon resources. - var iconResource = IconResource.FromDirectory(image.Resources!)!; - Assert.NotNull(iconResource); + private static IconResource GetTestIconResource(bool rebuild) + { + // Load dummy. + var image = PEImage.FromBytes(Properties.Resources.HelloWorld); - // Verify. - Assert.Single(iconResource.GetIconGroups()); - Assert.Equal(4, iconResource.GetIconGroups().ToList()[0].GetIconEntries().Count()); - } + // Read icon resources. + var iconResource = IconResource.FromDirectory(image.Resources!, IconType.Icon); + Assert.NotNull(iconResource); - [Fact] - public void PersistentIconResources() + if (rebuild) { - // Load dummy. - var image = PEImage.FromBytes(Properties.Resources.HelloWorld); - var resources = image.Resources!; - - // Update icon resources. - var iconResource = IconResource.FromDirectory(resources)!; - Assert.NotNull(iconResource); - - foreach (var iconGroup in iconResource.GetIconGroups()) - { - iconGroup.RemoveEntry(4); - iconGroup.Count--; - } - iconResource.InsertIntoDirectory(resources); - - // Rebuild. - using var stream = new MemoryStream(); - new ManagedPEFileBuilder().CreateFile(image).Write(new BinaryStreamWriter(stream)); - - // Reload version info. - var newImage = PEImage.FromBytes(stream.ToArray()); - var newIconResource = IconResource.FromDirectory(newImage.Resources!)!; - Assert.NotNull(newIconResource); - - // Verify. - Assert.Equal(iconResource.GetIconGroups().ToList()[0].Count, newIconResource.GetIconGroups().ToList()[0].Count); + iconResource.InsertIntoDirectory(image.Resources!); + iconResource = IconResource.FromDirectory(image.Resources!, IconType.Icon); } + + return iconResource; + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void IconGroup(bool rebuild) + { + var iconResource = GetTestIconResource(rebuild); + + // Verify. + var group = Assert.Single(iconResource.Groups); + Assert.Equal(32512u, group.Id); + Assert.Equal(0u, group.Lcid); + Assert.Equal(IconType.Icon, group.Type); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void IconEntries(bool rebuild) + { + var iconResource = GetTestIconResource(rebuild); + + // Verify. + var icons = iconResource.GetGroup(32512).Icons; + Assert.Equal([1, 2, 3, 4], icons.Select(x => x.Id)); + Assert.Equal([16, 32, 48, 64], icons.Select(x => x.Width)); + Assert.Equal([16, 32, 48, 64], icons.Select(x => x.Height)); + Assert.All(icons, icon => Assert.Equal(32, icon.BitsPerPixel)); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void IconData(bool rebuild) + { + var iconResource = GetTestIconResource(rebuild); + + // Verify. + var icons = iconResource.GetGroup(32512).Icons; + Assert.All(icons, icon => Assert.NotNull(icon.PixelData)); + } + + [Fact] + public void AddNewIcons() + { + var image = PEImage.FromBytes(Properties.Resources.HelloWorld); + var iconResource = IconResource.FromDirectory(image.Resources!, IconType.Icon)!; + + var iconGroup = new IconGroup(1337, 1033); + iconGroup.Icons.Add(new IconEntry(100, 1033) + { + Width = 10, + Height = 10, + BitsPerPixel = 32, + PixelData = new DataSegment([1, 2, 3, 4]), + }); + + iconResource.Groups.Add(iconGroup); + iconResource.InsertIntoDirectory(image.Resources!); + + var newIconResource = IconResource.FromDirectory(image.Resources!, IconType.Icon); + Assert.NotNull(newIconResource); + + Assert.Equal([32512, 1337], newIconResource.Groups.Select(x => x.Id)); + } + + [Fact] + public void RebuildPEWithIconResource() + { + // Load dummy. + var image = PEImage.FromBytes(Properties.Resources.HelloWorld); + + // Rebuild icon resources. + var iconResource = IconResource.FromDirectory(image.Resources!, IconType.Icon)!; + iconResource.InsertIntoDirectory(image.Resources!); + + var file = image.ToPEFile(new ManagedPEFileBuilder()); + _fixture + .GetRunner() + .RebuildAndRun(file, "HelloWorld.exe", "Hello World!\n"); } }