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();