Add PE menu resource reading and writing

This commit is contained in:
Matt Nadareski
2022-11-11 15:31:00 -08:00
parent 5b974260cc
commit f9e1518da6
8 changed files with 472 additions and 18 deletions

View File

@@ -465,7 +465,7 @@ namespace BurnOutSharp.Builder
/// <summary>
/// Read resource data as a dialog box
/// </summary>
/// <param name="entry">Resource data entry to parse into a font group</param>
/// <param name="entry">Resource data entry to parse into a dialog box</param>
/// <returns>A filled dialog box on success, null on error</returns>
public static Models.PortableExecutable.DialogBoxResource AsDialogBox(this Models.PortableExecutable.ResourceDataEntry entry)
{
@@ -1004,6 +1004,130 @@ namespace BurnOutSharp.Builder
return fontGroupHeader;
}
/// <summary>
/// Read resource data as a menu
/// </summary>
/// <param name="entry">Resource data entry to parse into a menu</param>
/// <returns>A filled menu on success, null on error</returns>
public static Models.PortableExecutable.MenuResource AsMenu(this Models.PortableExecutable.ResourceDataEntry entry)
{
// If we have an invalid entry, just skip
if (entry?.Data == null)
return null;
// Initialize the iterator
int offset = 0;
// Create the output object
var menuResource = new Models.PortableExecutable.MenuResource();
// Try to read the version for an extended header
int versionOffset = 0;
int possibleVersion = entry.Data.ReadUInt16(ref versionOffset);
if (possibleVersion == 0x0001)
{
#region Extended menu header
var menuHeaderExtended = new Models.PortableExecutable.MenuHeaderExtended();
menuHeaderExtended.Version = entry.Data.ReadUInt16(ref offset);
menuHeaderExtended.Offset = entry.Data.ReadUInt16(ref offset);
menuHeaderExtended.HelpID = entry.Data.ReadUInt32(ref offset);
menuResource.ExtendedMenuHeader = menuHeaderExtended;
#endregion
#region Extended dialog item templates
var extendedMenuItems = new List<Models.PortableExecutable.MenuItemExtended>();
if (offset != 0)
{
offset = menuHeaderExtended.Offset;
while (offset < entry.Data.Length)
{
var extendedMenuItem = new Models.PortableExecutable.MenuItemExtended();
extendedMenuItem.ItemType = (Models.PortableExecutable.MenuFlags)entry.Data.ReadUInt32(ref offset);
extendedMenuItem.State = (Models.PortableExecutable.MenuFlags)entry.Data.ReadUInt32(ref offset);
extendedMenuItem.ID = entry.Data.ReadUInt32(ref offset);
extendedMenuItem.Flags = (Models.PortableExecutable.MenuFlags)entry.Data.ReadUInt32(ref offset);
extendedMenuItem.MenuText = entry.Data.ReadString(ref offset, Encoding.Unicode);
// Align to the DWORD boundary if we're not at the end
if (offset != entry.Data.Length)
{
while ((offset % 4) != 0)
_ = entry.Data.ReadByte(ref offset);
}
extendedMenuItems.Add(extendedMenuItem);
}
}
menuResource.ExtendedMenuItems = extendedMenuItems.ToArray();
#endregion
}
else
{
#region Menu header
var menuHeader = new Models.PortableExecutable.MenuHeader();
menuHeader.Version = entry.Data.ReadUInt16(ref offset);
menuHeader.HeaderSize = entry.Data.ReadUInt16(ref offset);
menuResource.MenuHeader = menuHeader;
#endregion
#region Menu items
var menuItems = new List<Models.PortableExecutable.MenuItem>();
while (offset < entry.Data.Length)
{
var menuItem = new Models.PortableExecutable.MenuItem();
// Determine if this is a popup
int flagsOffset = offset;
var initialFlags = (Models.PortableExecutable.MenuFlags)entry.Data.ReadUInt16(ref flagsOffset);
if (initialFlags.HasFlag(Models.PortableExecutable.MenuFlags.MF_POPUP))
{
menuItem.PopupItemType = (Models.PortableExecutable.MenuFlags)entry.Data.ReadUInt32(ref offset);
menuItem.PopupState = (Models.PortableExecutable.MenuFlags)entry.Data.ReadUInt32(ref offset);
menuItem.PopupID = entry.Data.ReadUInt32(ref offset);
menuItem.PopupResInfo = (Models.PortableExecutable.MenuFlags)entry.Data.ReadUInt32(ref offset);
menuItem.PopupMenuText = entry.Data.ReadString(ref offset, Encoding.Unicode);
}
else
{
menuItem.NormalResInfo = (Models.PortableExecutable.MenuFlags)entry.Data.ReadUInt16(ref offset);
menuItem.NormalMenuText = entry.Data.ReadString(ref offset, Encoding.Unicode);
}
// Align to the DWORD boundary if we're not at the end
if (offset != entry.Data.Length)
{
while ((offset % 4) != 0)
_ = entry.Data.ReadByte(ref offset);
}
menuItems.Add(menuItem);
}
menuResource.MenuItems = menuItems.ToArray();
#endregion
}
return menuResource;
}
/// <summary>
/// Read resource data as a string table resource
/// </summary>

View File

@@ -1036,22 +1036,6 @@ namespace BurnOutSharp.Models.PortableExecutable
IMPORT_NAME_UNDECORATE = 3,
}
[Flags]
public enum MemoryFlags : ushort
{
// TODO: Validate the ~ statements
MOVEABLE = 0x0010,
FIXED = 0xFFEF, // ~MOVEABLE
PURE = 0x0020,
IMPURE = 0xFFDF, // ~PURE
PRELOAD = 0x0040,
LOADONCALL = 0xFFBF, // ~PRELOAD
DISCARDABLE = 0x1000,
}
public enum MachineType : ushort
{
/// <summary>
@@ -1190,6 +1174,83 @@ namespace BurnOutSharp.Models.PortableExecutable
IMAGE_FILE_MACHINE_WCEMIPSV2 = 0x0169,
}
[Flags]
public enum MemoryFlags : ushort
{
// TODO: Validate the ~ statements
MOVEABLE = 0x0010,
FIXED = 0xFFEF, // ~MOVEABLE
PURE = 0x0020,
IMPURE = 0xFFDF, // ~PURE
PRELOAD = 0x0040,
LOADONCALL = 0xFFBF, // ~PRELOAD
DISCARDABLE = 0x1000,
}
[Flags]
public enum MenuFlags : uint
{
MF_INSERT = 0x00000000,
MF_CHANGE = 0x00000080,
MF_APPEND = 0x00000100,
MF_DELETE = 0x00000200,
MF_REMOVE = 0x00001000,
MF_BYCOMMAND = 0x00000000,
MF_BYPOSITION = 0x00000400,
MF_SEPARATOR = 0x00000800,
MF_ENABLED = 0x00000000,
MF_GRAYED = 0x00000001,
MF_DISABLED = 0x00000002,
MF_UNCHECKED = 0x00000000,
MF_CHECKED = 0x00000008,
MF_USECHECKBITMAPS = 0x00000200,
MF_STRING = 0x00000000,
MF_BITMAP = 0x00000004,
MF_OWNERDRAW = 0x00000100,
MF_POPUP = 0x00000010,
MF_MENUBARBREAK = 0x00000020,
MF_MENUBREAK = 0x00000040,
MF_UNHILITE = 0x00000000,
MF_HILITE = 0x00000080,
MF_DEFAULT = 0x00001000,
MF_SYSMENU = 0x00002000,
MF_HELP = 0x00004000,
MF_RIGHTJUSTIFY = 0x00004000,
MF_MOUSESELECT = 0x00008000,
MF_END = 0x00000080,
MFT_STRING = MF_STRING,
MFT_BITMAP = MF_BITMAP,
MFT_MENUBARBREAK = MF_MENUBARBREAK,
MFT_MENUBREAK = MF_MENUBREAK,
MFT_OWNERDRAW = MF_OWNERDRAW,
MFT_RADIOCHECK = 0x00000200,
MFT_SEPARATOR = MF_SEPARATOR,
MFT_RIGHTORDER = 0x00002000,
MFT_RIGHTJUSTIFY = MF_RIGHTJUSTIFY,
MFS_GRAYED = 0x00000003,
MFS_DISABLED = MFS_GRAYED,
MFS_CHECKED = MF_CHECKED,
MFS_HILITE = MF_HILITE,
MFS_ENABLED = MF_ENABLED,
MFS_UNCHECKED = MF_UNCHECKED,
MFS_UNHILITE = MF_UNHILITE,
MFS_DEFAULT = MF_DEFAULT,
}
public enum OptionalHeaderMagicNumber : ushort
{
ROMImage = 0x0107,

View File

@@ -0,0 +1,25 @@
using System.Runtime.InteropServices;
namespace BurnOutSharp.Models.PortableExecutable
{
/// <summary>
/// Contains version information for the menu resource. The structure definition provided
/// here is for explanation only; it is not present in any standard header file.
/// </summary>
/// <see href="https://learn.microsoft.com/en-us/windows/win32/menurc/menuheader"/>
[StructLayout(LayoutKind.Sequential)]
public class MenuHeader
{
/// <summary>
/// The version number of the menu template. This member must be equal to zero to indicate
/// that this is an RT_MENU created with a standard menu template.
/// </summary>
public ushort Version;
/// <summary>
/// The size of the menu template header. This value is zero for menus you create with a
/// standard menu template.
/// </summary>
public ushort HeaderSize;
}
}

View File

@@ -0,0 +1,30 @@
using System.Runtime.InteropServices;
namespace BurnOutSharp.Models.PortableExecutable
{
/// <summary>
/// Defines the header for an extended menu template. This structure definition is for
/// explanation only; it is not present in any standard header file.
/// </summary>
/// <see href="https://learn.microsoft.com/en-us/windows/win32/menurc/menuex-template-header"/>
[StructLayout(LayoutKind.Sequential)]
public class MenuHeaderExtended
{
/// <summary>
/// The template version number. This member must be 1 for extended menu templates.
/// </summary>
public ushort Version;
/// <summary>
/// The offset to the first MENUEX_TEMPLATE_ITEM structure, relative to the end of
/// this structure member. If the first item definition immediately follows the
/// dwHelpId member, this member should be 4.
/// </summary>
public ushort Offset;
/// <summary>
/// The help identifier of menu bar.
/// </summary>
public uint HelpID;
}
}

View File

@@ -0,0 +1,62 @@
namespace BurnOutSharp.Models.PortableExecutable
{
/// <summary>
/// Contains information about each item in a menu resource that does not open a menu
/// or a submenu. The structure definition provided here is for explanation only; it
/// is not present in any standard header file.
///
/// Contains information about the menu items in a menu resource that open a menu
/// or a submenu. The structure definition provided here is for explanation only;
/// it is not present in any standard header file.
/// </summary>
/// <see href="https://learn.microsoft.com/en-us/windows/win32/menurc/normalmenuitem"/>
/// <see href="https://learn.microsoft.com/en-us/windows/win32/menurc/popupmenuitem"/>
public class MenuItem
{
#region NORMALMENUITEM
/// <summary>
/// The type of menu item.
/// </summary>
public MenuFlags NormalResInfo;
/// <summary>
/// A null-terminated Unicode string that contains the text for this menu item.
/// There is no fixed limit on the size of this string.
/// </summary>
public string NormalMenuText;
#endregion
#region POPUPMENUITEM
/// <summary>
/// Describes the menu item.
/// </summary>
public MenuFlags PopupItemType;
/// <summary>
/// Describes the menu item.
/// </summary>
public MenuFlags PopupState;
/// <summary>
/// A numeric expression that identifies the menu item that is passed in the
/// WM_COMMAND message.
/// </summary>
public uint PopupID;
/// <summary>
/// A set of bit flags that specify the type of menu item.
/// </summary>
public MenuFlags PopupResInfo;
/// <summary>
/// A null-terminated Unicode string that contains the text for this menu item.
/// There is no fixed limit on the size of this string.
/// </summary>
public string PopupMenuText;
#endregion
}
}

View File

@@ -0,0 +1,37 @@
namespace BurnOutSharp.Models.PortableExecutable
{
/// <summary>
/// Defines a menu item in an extended menu template. This structure definition is for
/// explanation only; it is not present in any standard header file.
/// </summary>
/// <see href="https://learn.microsoft.com/en-us/windows/win32/menurc/menuex-template-item"/>
public class MenuItemExtended
{
/// <summary>
/// Describes the menu item.
/// </summary>
public MenuFlags ItemType;
/// <summary>
/// Describes the menu item.
/// </summary>
public MenuFlags State;
/// <summary>
/// A numeric expression that identifies the menu item that is passed in the
/// WM_COMMAND message.
/// </summary>
public uint ID;
/// <summary>
/// A set of bit flags that specify the type of menu item.
/// </summary>
public MenuFlags Flags;
/// <summary>
/// A null-terminated Unicode string that contains the text for this menu item.
/// There is no fixed limit on the size of this string.
/// </summary>
public string MenuText;
}
}

View File

@@ -0,0 +1,40 @@
namespace BurnOutSharp.Models.PortableExecutable
{
/// <summary>
/// A menu resource consists of a MENUHEADER structure followed by one or more
/// NORMALMENUITEM or POPUPMENUITEM structures, one for each menu item in the menu
/// template. The MENUEX_TEMPLATE_HEADER and the MENUEX_TEMPLATE_ITEM structures
/// describe the format of extended menu resources.
/// </summary>
/// <see href="https://learn.microsoft.com/en-us/windows/win32/menurc/resource-file-formats"/>
public class MenuResource
{
#region Menu header
/// <summary>
/// Menu header structure
/// </summary>
public MenuHeader MenuHeader;
/// <summary>
/// Menu extended header structure
/// </summary>
public MenuHeaderExtended ExtendedMenuHeader;
#endregion
#region Menu items
/// <summary>
/// Menu items
/// </summary>
public MenuItem[] MenuItems;
/// <summary>
/// Extended menu items
/// </summary>
public MenuItemExtended[] ExtendedMenuItems;
#endregion
}
}

View File

@@ -1187,7 +1187,82 @@ namespace ExecutableTest
Console.WriteLine($"{padding}Hardware-dependent icon resource found, not parsed yet");
break;
case BurnOutSharp.Models.PortableExecutable.ResourceType.RT_MENU:
Console.WriteLine($"{padding}Menu resource found, not parsed yet");
var menu = entry.AsMenu();
if (menu == null)
{
Console.WriteLine($"{padding}Menu resource found, but malformed");
}
else
{
if (menu.MenuHeader != null)
{
Console.WriteLine($"{padding}Version: {menu.MenuHeader.Version}");
Console.WriteLine($"{padding}Header size: {menu.MenuHeader.HeaderSize}");
Console.WriteLine();
Console.WriteLine($"{padding}Menu items");
Console.WriteLine($"{padding}-------------------------");
if (menu.MenuItems == null
|| menu.MenuItems.Length == 0)
{
Console.WriteLine($"{padding}No menu items");
}
else
{
for (int i = 0; i < menu.MenuItems.Length; i++)
{
var menuItem = menu.MenuItems[i];
Console.WriteLine($"{padding}Menu item {i}");
if (menuItem.NormalMenuText != null)
{
Console.WriteLine($"{padding} Resource info: {menuItem.NormalResInfo}");
Console.WriteLine($"{padding} Menu text: {menuItem.NormalMenuText}");
}
else
{
Console.WriteLine($"{padding} Item type: {menuItem.PopupItemType}");
Console.WriteLine($"{padding} State: {menuItem.PopupState}");
Console.WriteLine($"{padding} ID: {menuItem.PopupID}");
Console.WriteLine($"{padding} Resource info: {menuItem.PopupResInfo}");
Console.WriteLine($"{padding} Menu text: {menuItem.PopupMenuText}");
}
}
}
}
else if (menu.ExtendedMenuHeader != null)
{
Console.WriteLine($"{padding}Version: {menu.ExtendedMenuHeader.Version}");
Console.WriteLine($"{padding}Offset: {menu.ExtendedMenuHeader.Offset}");
Console.WriteLine($"{padding}Help ID: {menu.ExtendedMenuHeader.HelpID}");
Console.WriteLine();
Console.WriteLine($"{padding}Menu items");
Console.WriteLine($"{padding}-------------------------");
if (menu.ExtendedMenuHeader.Offset == 0
|| menu.ExtendedMenuItems == null
|| menu.ExtendedMenuItems.Length == 0)
{
Console.WriteLine($"{padding}No menu items");
}
else
{
for (int i = 0; i < menu.ExtendedMenuItems.Length; i++)
{
var menuItem = menu.ExtendedMenuItems[i];
Console.WriteLine($"{padding}Dialog item template {i}");
Console.WriteLine($"{padding} Item type: {menuItem.ItemType}");
Console.WriteLine($"{padding} State: {menuItem.State}");
Console.WriteLine($"{padding} ID: {menuItem.ID}");
Console.WriteLine($"{padding} Flags: {menuItem.Flags}");
Console.WriteLine($"{padding} Menu text: {menuItem.MenuText}");
}
}
}
else
{
Console.WriteLine($"{padding}Menu resource found, but malformed");
}
}
break;
case BurnOutSharp.Models.PortableExecutable.ResourceType.RT_DIALOG:
var dialogBox = entry.AsDialogBox();