diff --git a/BurnOutSharp.Builder/Extensions.cs b/BurnOutSharp.Builder/Extensions.cs index cc0a067a..a4a65f43 100644 --- a/BurnOutSharp.Builder/Extensions.cs +++ b/BurnOutSharp.Builder/Extensions.cs @@ -465,7 +465,7 @@ namespace BurnOutSharp.Builder /// /// Read resource data as a dialog box /// - /// Resource data entry to parse into a font group + /// Resource data entry to parse into a dialog box /// A filled dialog box on success, null on error public static Models.PortableExecutable.DialogBoxResource AsDialogBox(this Models.PortableExecutable.ResourceDataEntry entry) { @@ -1004,6 +1004,130 @@ namespace BurnOutSharp.Builder return fontGroupHeader; } + + /// + /// Read resource data as a menu + /// + /// Resource data entry to parse into a menu + /// A filled menu on success, null on error + 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(); + + 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(); + + 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; + } + /// /// Read resource data as a string table resource /// diff --git a/BurnOutSharp.Models/PortableExecutable/Enums.cs b/BurnOutSharp.Models/PortableExecutable/Enums.cs index 6dc191db..02634c5f 100644 --- a/BurnOutSharp.Models/PortableExecutable/Enums.cs +++ b/BurnOutSharp.Models/PortableExecutable/Enums.cs @@ -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 { /// @@ -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, diff --git a/BurnOutSharp.Models/PortableExecutable/MenuHeader.cs b/BurnOutSharp.Models/PortableExecutable/MenuHeader.cs new file mode 100644 index 00000000..6f0b2699 --- /dev/null +++ b/BurnOutSharp.Models/PortableExecutable/MenuHeader.cs @@ -0,0 +1,25 @@ +using System.Runtime.InteropServices; + +namespace BurnOutSharp.Models.PortableExecutable +{ + /// + /// 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. + /// + /// + [StructLayout(LayoutKind.Sequential)] + public class MenuHeader + { + /// + /// 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. + /// + public ushort Version; + + /// + /// The size of the menu template header. This value is zero for menus you create with a + /// standard menu template. + /// + public ushort HeaderSize; + } +} diff --git a/BurnOutSharp.Models/PortableExecutable/MenuHeaderExtended.cs b/BurnOutSharp.Models/PortableExecutable/MenuHeaderExtended.cs new file mode 100644 index 00000000..5a05e14d --- /dev/null +++ b/BurnOutSharp.Models/PortableExecutable/MenuHeaderExtended.cs @@ -0,0 +1,30 @@ +using System.Runtime.InteropServices; + +namespace BurnOutSharp.Models.PortableExecutable +{ + /// + /// Defines the header for an extended menu template. This structure definition is for + /// explanation only; it is not present in any standard header file. + /// + /// + [StructLayout(LayoutKind.Sequential)] + public class MenuHeaderExtended + { + /// + /// The template version number. This member must be 1 for extended menu templates. + /// + public ushort Version; + + /// + /// 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. + /// + public ushort Offset; + + /// + /// The help identifier of menu bar. + /// + public uint HelpID; + } +} diff --git a/BurnOutSharp.Models/PortableExecutable/MenuItem.cs b/BurnOutSharp.Models/PortableExecutable/MenuItem.cs new file mode 100644 index 00000000..aebbd028 --- /dev/null +++ b/BurnOutSharp.Models/PortableExecutable/MenuItem.cs @@ -0,0 +1,62 @@ +namespace BurnOutSharp.Models.PortableExecutable +{ + /// + /// 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. + /// + /// + /// + public class MenuItem + { + #region NORMALMENUITEM + + /// + /// The type of menu item. + /// + public MenuFlags NormalResInfo; + + /// + /// A null-terminated Unicode string that contains the text for this menu item. + /// There is no fixed limit on the size of this string. + /// + public string NormalMenuText; + + #endregion + + #region POPUPMENUITEM + + /// + /// Describes the menu item. + /// + public MenuFlags PopupItemType; + + /// + /// Describes the menu item. + /// + public MenuFlags PopupState; + + /// + /// A numeric expression that identifies the menu item that is passed in the + /// WM_COMMAND message. + /// + public uint PopupID; + + /// + /// A set of bit flags that specify the type of menu item. + /// + public MenuFlags PopupResInfo; + + /// + /// A null-terminated Unicode string that contains the text for this menu item. + /// There is no fixed limit on the size of this string. + /// + public string PopupMenuText; + + #endregion + } +} diff --git a/BurnOutSharp.Models/PortableExecutable/MenuItemExtended.cs b/BurnOutSharp.Models/PortableExecutable/MenuItemExtended.cs new file mode 100644 index 00000000..cb560691 --- /dev/null +++ b/BurnOutSharp.Models/PortableExecutable/MenuItemExtended.cs @@ -0,0 +1,37 @@ +namespace BurnOutSharp.Models.PortableExecutable +{ + /// + /// 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. + /// + /// + public class MenuItemExtended + { + /// + /// Describes the menu item. + /// + public MenuFlags ItemType; + + /// + /// Describes the menu item. + /// + public MenuFlags State; + + /// + /// A numeric expression that identifies the menu item that is passed in the + /// WM_COMMAND message. + /// + public uint ID; + + /// + /// A set of bit flags that specify the type of menu item. + /// + public MenuFlags Flags; + + /// + /// A null-terminated Unicode string that contains the text for this menu item. + /// There is no fixed limit on the size of this string. + /// + public string MenuText; + } +} diff --git a/BurnOutSharp.Models/PortableExecutable/MenuResource.cs b/BurnOutSharp.Models/PortableExecutable/MenuResource.cs new file mode 100644 index 00000000..5605d198 --- /dev/null +++ b/BurnOutSharp.Models/PortableExecutable/MenuResource.cs @@ -0,0 +1,40 @@ +namespace BurnOutSharp.Models.PortableExecutable +{ + /// + /// 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. + /// + /// + public class MenuResource + { + #region Menu header + + /// + /// Menu header structure + /// + public MenuHeader MenuHeader; + + /// + /// Menu extended header structure + /// + public MenuHeaderExtended ExtendedMenuHeader; + + #endregion + + #region Menu items + + /// + /// Menu items + /// + public MenuItem[] MenuItems; + + /// + /// Extended menu items + /// + public MenuItemExtended[] ExtendedMenuItems; + + #endregion + } +} diff --git a/ExecutableTest/Program.cs b/ExecutableTest/Program.cs index df65c6b0..50fc5de4 100644 --- a/ExecutableTest/Program.cs +++ b/ExecutableTest/Program.cs @@ -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();