diff --git a/Enums.cs b/Enums.cs
new file mode 100644
index 00000000..4fe1c24d
--- /dev/null
+++ b/Enums.cs
@@ -0,0 +1,13 @@
+namespace SabreTools.Serialization
+{
+ public enum Hash
+ {
+ CRC,
+ MD5,
+ SHA1,
+ SHA256,
+ SHA384,
+ SHA512,
+ SpamSum,
+ }
+}
\ No newline at end of file
diff --git a/Extensions.cs b/Extensions.cs
new file mode 100644
index 00000000..895e93b6
--- /dev/null
+++ b/Extensions.cs
@@ -0,0 +1,1435 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Xml.Serialization;
+using SabreTools.IO;
+
+namespace SabreTools.Serialization
+{
+ public static class Extensions
+ {
+ #region New Executable
+
+ ///
+ /// Determine if a resource type information entry is an integer or offset
+ ///
+ /// Resource type information entry to check
+ /// True if the entry is an integer type, false if an offset, null on error
+ public static bool? IsIntegerType(this Models.NewExecutable.ResourceTypeInformationEntry entry)
+ {
+ // We can't do anything with an invalid entry
+ if (entry == null)
+ return null;
+
+ // If the highest order bit is set, it's an integer type
+ return (entry.TypeID & 0x8000) != 0;
+ }
+
+ ///
+ /// Determine if a resource type resource entry is an integer or offset
+ ///
+ /// Resource type resource entry to check
+ /// True if the entry is an integer type, false if an offset, null on error
+ public static bool? IsIntegerType(this Models.NewExecutable.ResourceTypeResourceEntry entry)
+ {
+ // We can't do anything with an invalid entry
+ if (entry == null)
+ return null;
+
+ // If the highest order bit is set, it's an integer type
+ return (entry.ResourceID & 0x8000) != 0;
+ }
+
+ ///
+ /// Get the segment entry type for an entry table bundle
+ ///
+ /// Entry table bundle to check
+ /// SegmentEntryType corresponding to the type
+ public static Models.NewExecutable.SegmentEntryType GetEntryType(this Models.NewExecutable.EntryTableBundle entry)
+ {
+ // We can't do anything with an invalid entry
+ if (entry == null)
+ return Models.NewExecutable.SegmentEntryType.Unused;
+
+ // Determine the entry type based on segment indicator
+ if (entry.SegmentIndicator == 0x00)
+ return Models.NewExecutable.SegmentEntryType.Unused;
+ else if (entry.SegmentIndicator >= 0x01 && entry.SegmentIndicator <= 0xFE)
+ return Models.NewExecutable.SegmentEntryType.FixedSegment;
+ else if (entry.SegmentIndicator == 0xFF)
+ return Models.NewExecutable.SegmentEntryType.MoveableSegment;
+
+ // We should never get here
+ return Models.NewExecutable.SegmentEntryType.Unused;
+ }
+
+ #endregion
+
+ #region Portable Executable
+
+ ///
+ /// Convert a relative virtual address to a physical one
+ ///
+ /// Relative virtual address to convert
+ /// Array of sections to check against
+ /// Physical address, 0 on error
+ public static uint ConvertVirtualAddress(this uint rva, Models.PortableExecutable.SectionHeader[] sections)
+ {
+ // If we have an invalid section table, we can't do anything
+ if (sections == null || sections.Length == 0)
+ return 0;
+
+ // If the RVA is 0, we just return 0 because it's invalid
+ if (rva == 0)
+ return 0;
+
+ // If the RVA matches a section start exactly, use that
+ var matchingSection = sections.FirstOrDefault(s => s.VirtualAddress == rva);
+ if (matchingSection != null)
+ return rva - matchingSection.VirtualAddress + matchingSection.PointerToRawData;
+
+ // Loop through all of the sections
+ for (int i = 0; i < sections.Length; i++)
+ {
+ // If the section is invalid, just skip it
+ if (sections[i] == null)
+ continue;
+
+ // If the section "starts" at 0, just skip it
+ if (sections[i].PointerToRawData == 0)
+ continue;
+
+ // Attempt to derive the physical address from the current section
+ var section = sections[i];
+ if (rva >= section.VirtualAddress && section.VirtualSize != 0 && rva <= section.VirtualAddress + section.VirtualSize)
+ return rva - section.VirtualAddress + section.PointerToRawData;
+ else if (rva >= section.VirtualAddress && section.SizeOfRawData != 0 && rva <= section.VirtualAddress + section.SizeOfRawData)
+ return rva - section.VirtualAddress + section.PointerToRawData;
+ }
+
+ return 0;
+ }
+
+ ///
+ /// Find the section a revlative virtual address lives in
+ ///
+ /// Relative virtual address to convert
+ /// Array of sections to check against
+ /// Section index, null on error
+ public static int ContainingSectionIndex(this uint rva, Models.PortableExecutable.SectionHeader[] sections)
+ {
+ // If we have an invalid section table, we can't do anything
+ if (sections == null || sections.Length == 0)
+ return -1;
+
+ // If the RVA is 0, we just return -1 because it's invalid
+ if (rva == 0)
+ return -1;
+
+ // Loop through all of the sections
+ for (int i = 0; i < sections.Length; i++)
+ {
+ // If the section is invalid, just skip it
+ if (sections[i] == null)
+ continue;
+
+ // If the section "starts" at 0, just skip it
+ if (sections[i].PointerToRawData == 0)
+ continue;
+
+ // Attempt to derive the physical address from the current section
+ var section = sections[i];
+ if (rva >= section.VirtualAddress && section.VirtualSize != 0 && rva <= section.VirtualAddress + section.VirtualSize)
+ return i;
+ else if (rva >= section.VirtualAddress && section.SizeOfRawData != 0 && rva <= section.VirtualAddress + section.SizeOfRawData)
+ return i;
+ }
+
+ return -1;
+ }
+
+ ///
+ /// Read overlay data as a SecuROM AddD overlay data
+ ///
+ /// Data to parse into overlay data
+ /// Offset into the byte array
+ /// A filled SecuROM AddD overlay data on success, null on error
+ public static Models.PortableExecutable.SecuROMAddD AsSecuROMAddD(this byte[] data, ref int offset)
+ {
+ // If we have data that's invalid, we can't do anything
+ if (data == null)
+ return null;
+
+ // Read in the table
+ var addD = new Models.PortableExecutable.SecuROMAddD();
+
+ addD.Signature = data.ReadUInt32(ref offset);
+ if (addD.Signature != 0x44646441)
+ return null;
+
+ int originalOffset = offset;
+
+ addD.EntryCount = data.ReadUInt32(ref offset);
+ addD.Version = data.ReadString(ref offset, Encoding.ASCII);
+ if (string.IsNullOrWhiteSpace(addD.Version))
+ offset = originalOffset + 0x10;
+
+ addD.Build = data.ReadBytes(ref offset, 4).Select(b => (char)b).ToArray();
+
+ // Distinguish between v1 and v2
+ int bytesToRead = 112; // v2
+ if (string.IsNullOrWhiteSpace(addD.Version)
+ || addD.Version.StartsWith("3")
+ || addD.Version.StartsWith("4.47"))
+ {
+ bytesToRead = 44;
+ }
+
+ addD.Unknown14h = data.ReadBytes(ref offset, bytesToRead);
+
+ addD.Entries = new Models.PortableExecutable.SecuROMAddDEntry[addD.EntryCount];
+ for (int i = 0; i < addD.EntryCount; i++)
+ {
+ var addDEntry = new Models.PortableExecutable.SecuROMAddDEntry();
+
+ addDEntry.PhysicalOffset = data.ReadUInt32(ref offset);
+ addDEntry.Length = data.ReadUInt32(ref offset);
+ addDEntry.Unknown08h = data.ReadUInt32(ref offset);
+ addDEntry.Unknown0Ch = data.ReadUInt32(ref offset);
+ addDEntry.Unknown10h = data.ReadUInt32(ref offset);
+ addDEntry.Unknown14h = data.ReadUInt32(ref offset);
+ addDEntry.Unknown18h = data.ReadUInt32(ref offset);
+ addDEntry.Unknown1Ch = data.ReadUInt32(ref offset);
+ addDEntry.FileName = data.ReadString(ref offset, Encoding.ASCII);
+ addDEntry.Unknown2Ch = data.ReadUInt32(ref offset);
+
+ addD.Entries[i] = addDEntry;
+ }
+
+ return addD;
+ }
+
+ #region Debug
+
+ ///
+ /// Read debug data as an NB10 Program Database
+ ///
+ /// Data to parse into a database
+ /// Offset into the byte array
+ /// A filled NB10 Program Database on success, null on error
+ public static Models.PortableExecutable.NB10ProgramDatabase AsNB10ProgramDatabase(this byte[] data, ref int offset)
+ {
+ // If we have data that's invalid, we can't do anything
+ if (data == null)
+ return null;
+
+ var nb10ProgramDatabase = new Models.PortableExecutable.NB10ProgramDatabase();
+
+ nb10ProgramDatabase.Signature = data.ReadUInt32(ref offset);
+ if (nb10ProgramDatabase.Signature != 0x3031424E)
+ return null;
+
+ nb10ProgramDatabase.Offset = data.ReadUInt32(ref offset);
+ nb10ProgramDatabase.Timestamp = data.ReadUInt32(ref offset);
+ nb10ProgramDatabase.Age = data.ReadUInt32(ref offset);
+ nb10ProgramDatabase.PdbFileName = data.ReadString(ref offset, Encoding.ASCII); // TODO: Actually null-terminated UTF-8?
+
+ return nb10ProgramDatabase;
+ }
+
+ ///
+ /// Read debug data as an RSDS Program Database
+ ///
+ /// Data to parse into a database
+ /// Offset into the byte array
+ /// A filled RSDS Program Database on success, null on error
+ public static Models.PortableExecutable.RSDSProgramDatabase AsRSDSProgramDatabase(this byte[] data, ref int offset)
+ {
+ // If we have data that's invalid, we can't do anything
+ if (data == null)
+ return null;
+
+ var rsdsProgramDatabase = new Models.PortableExecutable.RSDSProgramDatabase();
+
+ rsdsProgramDatabase.Signature = data.ReadUInt32(ref offset);
+ if (rsdsProgramDatabase.Signature != 0x53445352)
+ return null;
+
+ rsdsProgramDatabase.GUID = new Guid(data.ReadBytes(ref offset, 0x10));
+ rsdsProgramDatabase.Age = data.ReadUInt32(ref offset);
+ rsdsProgramDatabase.PathAndFileName = data.ReadString(ref offset, Encoding.ASCII); // TODO: Actually null-terminated UTF-8
+
+ return rsdsProgramDatabase;
+ }
+
+ #endregion
+
+ // TODO: Implement other resource types from https://learn.microsoft.com/en-us/windows/win32/menurc/resource-file-formats
+ #region Resources
+
+ ///
+ /// Read resource data as a resource header
+ ///
+ /// Data to parse into a resource header
+ /// Offset into the byte array
+ /// A filled resource header on success, null on error
+ public static Models.PortableExecutable.ResourceHeader AsResourceHeader(this byte[] data, ref int offset)
+ {
+ // If we have data that's invalid, we can't do anything
+ if (data == null)
+ return null;
+
+ // Read in the table
+ var header = new Models.PortableExecutable.ResourceHeader();
+ header.DataSize = data.ReadUInt32(ref offset);
+ header.HeaderSize = data.ReadUInt32(ref offset);
+ header.ResourceType = (Models.PortableExecutable.ResourceType)data.ReadUInt32(ref offset); // TODO: Could be a string too
+ header.Name = data.ReadUInt32(ref offset); // TODO: Could be a string too
+ header.DataVersion = data.ReadUInt32(ref offset);
+ header.MemoryFlags = (Models.PortableExecutable.MemoryFlags)data.ReadUInt16(ref offset);
+ header.LanguageId = data.ReadUInt16(ref offset);
+ header.Version = data.ReadUInt32(ref offset);
+ header.Characteristics = data.ReadUInt32(ref offset);
+
+ return header;
+ }
+
+ ///
+ /// Read resource data as an accelerator table resource
+ ///
+ /// Resource data entry to parse into an accelerator table resource
+ /// A filled accelerator table resource on success, null on error
+ public static Models.PortableExecutable.AcceleratorTableEntry[] AsAcceleratorTableResource(this Models.PortableExecutable.ResourceDataEntry entry)
+ {
+ // If we have data that's invalid for this resource type, we can't do anything
+ if (entry?.Data == null || entry.Data.Length % 8 != 0)
+ return null;
+
+ // Get the number of entries
+ int count = entry.Data.Length / 8;
+
+ // Initialize the iterator
+ int offset = 0;
+
+ // Create the output object
+ var table = new Models.PortableExecutable.AcceleratorTableEntry[count];
+
+ // Read in the table
+ for (int i = 0; i < count; i++)
+ {
+ var acceleratorTableEntry = new Models.PortableExecutable.AcceleratorTableEntry();
+
+ acceleratorTableEntry.Flags = (Models.PortableExecutable.AcceleratorTableFlags)entry.Data.ReadUInt16(ref offset);
+ acceleratorTableEntry.Ansi = entry.Data.ReadUInt16(ref offset);
+ acceleratorTableEntry.Id = entry.Data.ReadUInt16(ref offset);
+ acceleratorTableEntry.Padding = entry.Data.ReadUInt16(ref offset);
+
+ table[i] = acceleratorTableEntry;
+ }
+
+ return table;
+ }
+
+ ///
+ /// Read resource data as a side-by-side assembly manifest
+ ///
+ /// Resource data entry to parse into a side-by-side assembly manifest
+ /// A filled side-by-side assembly manifest on success, null on error
+ public static Models.PortableExecutable.AssemblyManifest AsAssemblyManifest(this Models.PortableExecutable.ResourceDataEntry entry)
+ {
+ // If we have an invalid entry, just skip
+ if (entry?.Data == null)
+ return null;
+
+ try
+ {
+ XmlSerializer serializer = new XmlSerializer(typeof(Models.PortableExecutable.AssemblyManifest));
+ return serializer.Deserialize(new MemoryStream(entry.Data)) as Models.PortableExecutable.AssemblyManifest;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ ///
+ /// Read resource data as a dialog box
+ ///
+ /// 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)
+ {
+ // 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 dialogBoxResource = new Models.PortableExecutable.DialogBoxResource();
+
+ // Try to read the signature for an extended dialog box template
+ int signatureOffset = sizeof(ushort);
+ int possibleSignature = entry.Data.ReadUInt16(ref signatureOffset);
+ if (possibleSignature == 0xFFFF)
+ {
+ #region Extended dialog template
+
+ var dialogTemplateExtended = new Models.PortableExecutable.DialogTemplateExtended();
+
+ dialogTemplateExtended.Version = entry.Data.ReadUInt16(ref offset);
+ dialogTemplateExtended.Signature = entry.Data.ReadUInt16(ref offset);
+ dialogTemplateExtended.HelpID = entry.Data.ReadUInt32(ref offset);
+ dialogTemplateExtended.ExtendedStyle = (Models.PortableExecutable.ExtendedWindowStyles)entry.Data.ReadUInt32(ref offset);
+ dialogTemplateExtended.Style = (Models.PortableExecutable.WindowStyles)entry.Data.ReadUInt32(ref offset);
+ dialogTemplateExtended.DialogItems = entry.Data.ReadUInt16(ref offset);
+ dialogTemplateExtended.PositionX = entry.Data.ReadInt16(ref offset);
+ dialogTemplateExtended.PositionY = entry.Data.ReadInt16(ref offset);
+ dialogTemplateExtended.WidthX = entry.Data.ReadInt16(ref offset);
+ dialogTemplateExtended.HeightY = entry.Data.ReadInt16(ref offset);
+
+ #region Menu resource
+
+ int currentOffset = offset;
+ ushort menuResourceIdentifier = entry.Data.ReadUInt16(ref offset);
+ offset = currentOffset;
+
+ // 0x0000 means no elements
+ if (menuResourceIdentifier == 0x0000)
+ {
+ // Increment the pointer if it was empty
+ offset += sizeof(ushort);
+ }
+ else
+ {
+ // Flag if there's an ordinal at the end
+ bool menuResourceHasOrdinal = menuResourceIdentifier == 0xFFFF;
+ if (menuResourceHasOrdinal)
+ offset += sizeof(ushort);
+
+ // Read the menu resource as a string
+ dialogTemplateExtended.MenuResource = entry.Data.ReadString(ref offset, Encoding.Unicode);
+
+ // Align to the WORD boundary if we're not at the end
+ if (offset != entry.Data.Length)
+ {
+ while ((offset % 2) != 0)
+ _ = entry.Data.ReadByte(ref offset);
+ }
+
+ // Read the ordinal if we have the flag set
+ if (menuResourceHasOrdinal)
+ dialogTemplateExtended.MenuResourceOrdinal = entry.Data.ReadUInt16(ref offset);
+ }
+
+ #endregion
+
+ #region Class resource
+
+ currentOffset = offset;
+ ushort classResourceIdentifier = entry.Data.ReadUInt16(ref offset);
+ offset = currentOffset;
+
+ // 0x0000 means no elements
+ if (classResourceIdentifier == 0x0000)
+ {
+ // Increment the pointer if it was empty
+ offset += sizeof(ushort);
+ }
+ else
+ {
+ // Flag if there's an ordinal at the end
+ bool classResourcehasOrdinal = classResourceIdentifier == 0xFFFF;
+ if (classResourcehasOrdinal)
+ offset += sizeof(ushort);
+
+ // Read the class resource as a string
+ dialogTemplateExtended.ClassResource = entry.Data.ReadString(ref offset, Encoding.Unicode);
+
+ // Align to the WORD boundary if we're not at the end
+ if (offset != entry.Data.Length)
+ {
+ while ((offset % 2) != 0)
+ _ = entry.Data.ReadByte(ref offset);
+ }
+
+ // Read the ordinal if we have the flag set
+ if (classResourcehasOrdinal)
+ dialogTemplateExtended.ClassResourceOrdinal = entry.Data.ReadUInt16(ref offset);
+ }
+
+ #endregion
+
+ #region Title resource
+
+ currentOffset = offset;
+ ushort titleResourceIdentifier = entry.Data.ReadUInt16(ref offset);
+ offset = currentOffset;
+
+ // 0x0000 means no elements
+ if (titleResourceIdentifier == 0x0000)
+ {
+ // Increment the pointer if it was empty
+ offset += sizeof(ushort);
+ }
+ else
+ {
+ // Read the title resource as a string
+ dialogTemplateExtended.TitleResource = entry.Data.ReadString(ref offset, Encoding.Unicode);
+
+ // Align to the WORD boundary if we're not at the end
+ if (offset != entry.Data.Length)
+ {
+ while ((offset % 2) != 0)
+ _ = entry.Data.ReadByte(ref offset);
+ }
+ }
+
+ #endregion
+
+ #region Point size and typeface
+
+ // Only if DS_SETFONT is set are the values here used
+ if (dialogTemplateExtended.Style.HasFlag(Models.PortableExecutable.WindowStyles.DS_SETFONT))
+ {
+ dialogTemplateExtended.PointSize = entry.Data.ReadUInt16(ref offset);
+ dialogTemplateExtended.Weight = entry.Data.ReadUInt16(ref offset);
+ dialogTemplateExtended.Italic = entry.Data.ReadByte(ref offset);
+ dialogTemplateExtended.CharSet = entry.Data.ReadByte(ref offset);
+ dialogTemplateExtended.Typeface = 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);
+ }
+
+ #endregion
+
+ dialogBoxResource.ExtendedDialogTemplate = dialogTemplateExtended;
+
+ #endregion
+
+ #region Extended dialog item templates
+
+ var dialogItemExtendedTemplates = new List();
+
+ for (int i = 0; i < dialogTemplateExtended.DialogItems; i++)
+ {
+ var dialogItemTemplate = new Models.PortableExecutable.DialogItemTemplateExtended();
+
+ dialogItemTemplate.HelpID = entry.Data.ReadUInt32(ref offset);
+ dialogItemTemplate.ExtendedStyle = (Models.PortableExecutable.ExtendedWindowStyles)entry.Data.ReadUInt32(ref offset);
+ dialogItemTemplate.Style = (Models.PortableExecutable.WindowStyles)entry.Data.ReadUInt32(ref offset);
+ dialogItemTemplate.PositionX = entry.Data.ReadInt16(ref offset);
+ dialogItemTemplate.PositionY = entry.Data.ReadInt16(ref offset);
+ dialogItemTemplate.WidthX = entry.Data.ReadInt16(ref offset);
+ dialogItemTemplate.HeightY = entry.Data.ReadInt16(ref offset);
+ dialogItemTemplate.ID = entry.Data.ReadUInt32(ref offset);
+
+ #region Class resource
+
+ currentOffset = offset;
+ ushort itemClassResourceIdentifier = entry.Data.ReadUInt16(ref offset);
+ offset = currentOffset;
+
+ // 0xFFFF means ordinal only
+ if (itemClassResourceIdentifier == 0xFFFF)
+ {
+ // Increment the pointer
+ _ = entry.Data.ReadUInt16(ref offset);
+
+ // Read the ordinal
+ dialogItemTemplate.ClassResourceOrdinal = (Models.PortableExecutable.DialogItemTemplateOrdinal)entry.Data.ReadUInt16(ref offset);
+ }
+ else
+ {
+ // Flag if there's an ordinal at the end
+ bool classResourcehasOrdinal = itemClassResourceIdentifier == 0xFFFF;
+ if (classResourcehasOrdinal)
+ offset += sizeof(ushort);
+
+ // Read the class resource as a string
+ dialogItemTemplate.ClassResource = entry.Data.ReadString(ref offset, Encoding.Unicode);
+
+ // Align to the WORD boundary if we're not at the end
+ if (offset != entry.Data.Length)
+ {
+ while ((offset % 2) != 0)
+ _ = entry.Data.ReadByte(ref offset);
+ }
+ }
+
+ #endregion
+
+ #region Title resource
+
+ currentOffset = offset;
+ ushort itemTitleResourceIdentifier = entry.Data.ReadUInt16(ref offset);
+ offset = currentOffset;
+
+ // 0xFFFF means ordinal only
+ if (itemTitleResourceIdentifier == 0xFFFF)
+ {
+ // Increment the pointer
+ _ = entry.Data.ReadUInt16(ref offset);
+
+ // Read the ordinal
+ dialogItemTemplate.TitleResourceOrdinal = entry.Data.ReadUInt16(ref offset);
+ }
+ else
+ {
+ // Read the title resource as a string
+ dialogItemTemplate.TitleResource = entry.Data.ReadString(ref offset, Encoding.Unicode);
+
+ // Align to the WORD boundary if we're not at the end
+ if (offset != entry.Data.Length)
+ {
+ while ((offset % 2) != 0)
+ _ = entry.Data.ReadByte(ref offset);
+ }
+ }
+
+ #endregion
+
+ #region Creation data
+
+ dialogItemTemplate.CreationDataSize = entry.Data.ReadUInt16(ref offset);
+ if (dialogItemTemplate.CreationDataSize != 0)
+ dialogItemTemplate.CreationData = entry.Data.ReadBytes(ref offset, dialogItemTemplate.CreationDataSize);
+
+ #endregion
+
+ // 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);
+ }
+
+ dialogItemExtendedTemplates.Add(dialogItemTemplate);
+ }
+
+ dialogBoxResource.ExtendedDialogItemTemplates = dialogItemExtendedTemplates.ToArray();
+
+ #endregion
+ }
+ else
+ {
+ #region Dialog template
+
+ var dialogTemplate = new Models.PortableExecutable.DialogTemplate();
+
+ dialogTemplate.Style = (Models.PortableExecutable.WindowStyles)entry.Data.ReadUInt32(ref offset);
+ dialogTemplate.ExtendedStyle = (Models.PortableExecutable.ExtendedWindowStyles)entry.Data.ReadUInt32(ref offset);
+ dialogTemplate.ItemCount = entry.Data.ReadUInt16(ref offset);
+ dialogTemplate.PositionX = entry.Data.ReadInt16(ref offset);
+ dialogTemplate.PositionY = entry.Data.ReadInt16(ref offset);
+ dialogTemplate.WidthX = entry.Data.ReadInt16(ref offset);
+ dialogTemplate.HeightY = entry.Data.ReadInt16(ref offset);
+
+ #region Menu resource
+
+ int currentOffset = offset;
+ ushort menuResourceIdentifier = entry.Data.ReadUInt16(ref offset);
+ offset = currentOffset;
+
+ // 0x0000 means no elements
+ if (menuResourceIdentifier == 0x0000)
+ {
+ // Increment the pointer if it was empty
+ offset += sizeof(ushort);
+ }
+ else
+ {
+ // Flag if there's an ordinal at the end
+ bool menuResourceHasOrdinal = menuResourceIdentifier == 0xFFFF;
+ if (menuResourceHasOrdinal)
+ offset += sizeof(ushort);
+
+ // Read the menu resource as a string
+ dialogTemplate.MenuResource = entry.Data.ReadString(ref offset, Encoding.Unicode);
+
+ // Align to the WORD boundary if we're not at the end
+ if (offset != entry.Data.Length)
+ {
+ while ((offset % 2) != 0)
+ _ = entry.Data.ReadByte(ref offset);
+ }
+
+ // Read the ordinal if we have the flag set
+ if (menuResourceHasOrdinal)
+ dialogTemplate.MenuResourceOrdinal = entry.Data.ReadUInt16(ref offset);
+ }
+
+ #endregion
+
+ #region Class resource
+
+ currentOffset = offset;
+ ushort classResourceIdentifier = entry.Data.ReadUInt16(ref offset);
+ offset = currentOffset;
+
+ // 0x0000 means no elements
+ if (classResourceIdentifier == 0x0000)
+ {
+ // Increment the pointer if it was empty
+ offset += sizeof(ushort);
+ }
+ else
+ {
+ // Flag if there's an ordinal at the end
+ bool classResourcehasOrdinal = classResourceIdentifier == 0xFFFF;
+ if (classResourcehasOrdinal)
+ offset += sizeof(ushort);
+
+ // Read the class resource as a string
+ dialogTemplate.ClassResource = entry.Data.ReadString(ref offset, Encoding.Unicode);
+
+ // Align to the WORD boundary if we're not at the end
+ if (offset != entry.Data.Length)
+ {
+ while ((offset % 2) != 0)
+ _ = entry.Data.ReadByte(ref offset);
+ }
+
+ // Read the ordinal if we have the flag set
+ if (classResourcehasOrdinal)
+ dialogTemplate.ClassResourceOrdinal = entry.Data.ReadUInt16(ref offset);
+ }
+
+ #endregion
+
+ #region Title resource
+
+ currentOffset = offset;
+ ushort titleResourceIdentifier = entry.Data.ReadUInt16(ref offset);
+ offset = currentOffset;
+
+ // 0x0000 means no elements
+ if (titleResourceIdentifier == 0x0000)
+ {
+ // Increment the pointer if it was empty
+ offset += sizeof(ushort);
+ }
+ else
+ {
+ // Read the title resource as a string
+ dialogTemplate.TitleResource = entry.Data.ReadString(ref offset, Encoding.Unicode);
+
+ // Align to the WORD boundary if we're not at the end
+ if (offset != entry.Data.Length)
+ {
+ while ((offset % 2) != 0)
+ _ = entry.Data.ReadByte(ref offset);
+ }
+ }
+
+ #endregion
+
+ #region Point size and typeface
+
+ // Only if DS_SETFONT is set are the values here used
+ if (dialogTemplate.Style.HasFlag(Models.PortableExecutable.WindowStyles.DS_SETFONT))
+ {
+ dialogTemplate.PointSizeValue = entry.Data.ReadUInt16(ref offset);
+
+ // Read the font name as a string
+ dialogTemplate.Typeface = 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);
+ }
+
+ #endregion
+
+ dialogBoxResource.DialogTemplate = dialogTemplate;
+
+ #endregion
+
+ #region Dialog item templates
+
+ var dialogItemTemplates = new List();
+
+ for (int i = 0; i < dialogTemplate.ItemCount; i++)
+ {
+ var dialogItemTemplate = new Models.PortableExecutable.DialogItemTemplate();
+
+ dialogItemTemplate.Style = (Models.PortableExecutable.WindowStyles)entry.Data.ReadUInt32(ref offset);
+ dialogItemTemplate.ExtendedStyle = (Models.PortableExecutable.ExtendedWindowStyles)entry.Data.ReadUInt32(ref offset);
+ dialogItemTemplate.PositionX = entry.Data.ReadInt16(ref offset);
+ dialogItemTemplate.PositionY = entry.Data.ReadInt16(ref offset);
+ dialogItemTemplate.WidthX = entry.Data.ReadInt16(ref offset);
+ dialogItemTemplate.HeightY = entry.Data.ReadInt16(ref offset);
+ dialogItemTemplate.ID = entry.Data.ReadUInt16(ref offset);
+
+ #region Class resource
+
+ currentOffset = offset;
+ ushort itemClassResourceIdentifier = entry.Data.ReadUInt16(ref offset);
+ offset = currentOffset;
+
+ // 0xFFFF means ordinal only
+ if (itemClassResourceIdentifier == 0xFFFF)
+ {
+ // Increment the pointer
+ _ = entry.Data.ReadUInt16(ref offset);
+
+ // Read the ordinal
+ dialogItemTemplate.ClassResourceOrdinal = (Models.PortableExecutable.DialogItemTemplateOrdinal)entry.Data.ReadUInt16(ref offset);
+ }
+ else
+ {
+ // Flag if there's an ordinal at the end
+ bool classResourcehasOrdinal = itemClassResourceIdentifier == 0xFFFF;
+ if (classResourcehasOrdinal)
+ offset += sizeof(ushort);
+
+ // Read the class resource as a string
+ dialogItemTemplate.ClassResource = entry.Data.ReadString(ref offset, Encoding.Unicode);
+
+ // Align to the WORD boundary if we're not at the end
+ if (offset != entry.Data.Length)
+ {
+ while ((offset % 2) != 0)
+ _ = entry.Data.ReadByte(ref offset);
+ }
+ }
+
+ #endregion
+
+ #region Title resource
+
+ currentOffset = offset;
+ ushort itemTitleResourceIdentifier = entry.Data.ReadUInt16(ref offset);
+ offset = currentOffset;
+
+ // 0xFFFF means ordinal only
+ if (itemTitleResourceIdentifier == 0xFFFF)
+ {
+ // Increment the pointer
+ _ = entry.Data.ReadUInt16(ref offset);
+
+ // Read the ordinal
+ dialogItemTemplate.TitleResourceOrdinal = entry.Data.ReadUInt16(ref offset);
+ }
+ else
+ {
+ // Read the title resource as a string
+ dialogItemTemplate.TitleResource = entry.Data.ReadString(ref offset, Encoding.Unicode);
+
+ // Align to the WORD boundary if we're not at the end
+ if (offset != entry.Data.Length)
+ {
+ while ((offset % 2) != 0)
+ _ = entry.Data.ReadByte(ref offset);
+ }
+ }
+
+ #endregion
+
+ #region Creation data
+
+ dialogItemTemplate.CreationDataSize = entry.Data.ReadUInt16(ref offset);
+ if (dialogItemTemplate.CreationDataSize != 0)
+ dialogItemTemplate.CreationData = entry.Data.ReadBytes(ref offset, dialogItemTemplate.CreationDataSize);
+
+ #endregion
+
+ // 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);
+ }
+
+ dialogItemTemplates.Add(dialogItemTemplate);
+ }
+
+ dialogBoxResource.DialogItemTemplates = dialogItemTemplates.ToArray();
+
+ #endregion
+ }
+
+ return dialogBoxResource;
+ }
+
+ ///
+ /// Read resource data as a font group
+ ///
+ /// Resource data entry to parse into a font group
+ /// A filled font group on success, null on error
+ public static Models.PortableExecutable.FontGroupHeader AsFontGroup(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 fontGroupHeader = new Models.PortableExecutable.FontGroupHeader();
+
+ fontGroupHeader.NumberOfFonts = entry.Data.ReadUInt16(ref offset);
+ if (fontGroupHeader.NumberOfFonts > 0)
+ {
+ fontGroupHeader.DE = new Models.PortableExecutable.DirEntry[fontGroupHeader.NumberOfFonts];
+ for (int i = 0; i < fontGroupHeader.NumberOfFonts; i++)
+ {
+ var dirEntry = new Models.PortableExecutable.DirEntry();
+
+ dirEntry.FontOrdinal = entry.Data.ReadUInt16(ref offset);
+
+ dirEntry.Entry = new Models.PortableExecutable.FontDirEntry();
+ dirEntry.Entry.Version = entry.Data.ReadUInt16(ref offset);
+ dirEntry.Entry.Size = entry.Data.ReadUInt32(ref offset);
+ dirEntry.Entry.Copyright = entry.Data.ReadBytes(ref offset, 60);
+ dirEntry.Entry.Type = entry.Data.ReadUInt16(ref offset);
+ dirEntry.Entry.Points = entry.Data.ReadUInt16(ref offset);
+ dirEntry.Entry.VertRes = entry.Data.ReadUInt16(ref offset);
+ dirEntry.Entry.HorizRes = entry.Data.ReadUInt16(ref offset);
+ dirEntry.Entry.Ascent = entry.Data.ReadUInt16(ref offset);
+ dirEntry.Entry.InternalLeading = entry.Data.ReadUInt16(ref offset);
+ dirEntry.Entry.ExternalLeading = entry.Data.ReadUInt16(ref offset);
+ dirEntry.Entry.Italic = entry.Data.ReadByte(ref offset);
+ dirEntry.Entry.Underline = entry.Data.ReadByte(ref offset);
+ dirEntry.Entry.StrikeOut = entry.Data.ReadByte(ref offset);
+ dirEntry.Entry.Weight = entry.Data.ReadUInt16(ref offset);
+ dirEntry.Entry.CharSet = entry.Data.ReadByte(ref offset);
+ dirEntry.Entry.PixWidth = entry.Data.ReadUInt16(ref offset);
+ dirEntry.Entry.PixHeight = entry.Data.ReadUInt16(ref offset);
+ dirEntry.Entry.PitchAndFamily = entry.Data.ReadByte(ref offset);
+ dirEntry.Entry.AvgWidth = entry.Data.ReadUInt16(ref offset);
+ dirEntry.Entry.MaxWidth = entry.Data.ReadUInt16(ref offset);
+ dirEntry.Entry.FirstChar = entry.Data.ReadByte(ref offset);
+ dirEntry.Entry.LastChar = entry.Data.ReadByte(ref offset);
+ dirEntry.Entry.DefaultChar = entry.Data.ReadByte(ref offset);
+ dirEntry.Entry.BreakChar = entry.Data.ReadByte(ref offset);
+ dirEntry.Entry.WidthBytes = entry.Data.ReadUInt16(ref offset);
+ dirEntry.Entry.Device = entry.Data.ReadUInt32(ref offset);
+ dirEntry.Entry.Face = entry.Data.ReadUInt32(ref offset);
+ dirEntry.Entry.Reserved = entry.Data.ReadUInt32(ref offset);
+
+ // TODO: Determine how to read these two? Immediately after?
+ dirEntry.Entry.DeviceName = entry.Data.ReadString(ref offset);
+ dirEntry.Entry.FaceName = entry.Data.ReadString(ref offset);
+
+ fontGroupHeader.DE[i] = dirEntry;
+ }
+ }
+
+ // TODO: Implement entry parsing
+ 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 message table resource
+ ///
+ /// Resource data entry to parse into a message table resource
+ /// A filled message table resource on success, null on error
+ public static Models.PortableExecutable.MessageResourceData AsMessageResourceData(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 messageResourceData = new Models.PortableExecutable.MessageResourceData();
+
+ // Message resource blocks
+ messageResourceData.NumberOfBlocks = entry.Data.ReadUInt32(ref offset);
+ if (messageResourceData.NumberOfBlocks > 0)
+ {
+ var messageResourceBlocks = new List();
+
+ for (int i = 0; i < messageResourceData.NumberOfBlocks; i++)
+ {
+ var messageResourceBlock = new Models.PortableExecutable.MessageResourceBlock();
+
+ messageResourceBlock.LowId = entry.Data.ReadUInt32(ref offset);
+ messageResourceBlock.HighId = entry.Data.ReadUInt32(ref offset);
+ messageResourceBlock.OffsetToEntries = entry.Data.ReadUInt32(ref offset);
+
+ messageResourceBlocks.Add(messageResourceBlock);
+ }
+
+ messageResourceData.Blocks = messageResourceBlocks.ToArray();
+ }
+
+ // Message resource entries
+ if (messageResourceData.Blocks != null && messageResourceData.Blocks.Length != 0)
+ {
+ var messageResourceEntries = new Dictionary();
+
+ for (int i = 0; i < messageResourceData.Blocks.Length; i++)
+ {
+ var messageResourceBlock = messageResourceData.Blocks[i];
+ offset = (int)messageResourceBlock.OffsetToEntries;
+
+ for (uint j = messageResourceBlock.LowId; j <= messageResourceBlock.HighId; j++)
+ {
+ var messageResourceEntry = new Models.PortableExecutable.MessageResourceEntry();
+
+ messageResourceEntry.Length = entry.Data.ReadUInt16(ref offset);
+ messageResourceEntry.Flags = entry.Data.ReadUInt16(ref offset);
+
+ Encoding textEncoding = messageResourceEntry.Flags == 0x0001 ? Encoding.Unicode : Encoding.ASCII;
+ byte[] textArray = entry.Data.ReadBytes(ref offset, messageResourceEntry.Length - 4);
+ messageResourceEntry.Text = textEncoding.GetString(textArray);
+
+ messageResourceEntries[j] = messageResourceEntry;
+ }
+ }
+
+ messageResourceData.Entries = messageResourceEntries;
+ }
+
+ return messageResourceData;
+ }
+
+ ///
+ /// Read resource data as a string table resource
+ ///
+ /// Resource data entry to parse into a string table resource
+ /// A filled string table resource on success, null on error
+ public static Dictionary AsStringTable(this Models.PortableExecutable.ResourceDataEntry entry)
+ {
+ // If we have an invalid entry, just skip
+ if (entry?.Data == null)
+ return null;
+
+ // Initialize the iterators
+ int offset = 0, stringIndex = 0;
+
+ // Create the output table
+ var stringTable = new Dictionary();
+
+ // Loop through and add
+ while (offset < entry.Data.Length)
+ {
+ ushort stringLength = entry.Data.ReadUInt16(ref offset);
+ if (stringLength == 0)
+ {
+ stringTable[stringIndex++] = "[EMPTY]";
+ }
+ else
+ {
+ if (stringLength * 2 > entry.Data.Length - offset)
+ {
+ Console.WriteLine($"{stringLength * 2} requested but {entry.Data.Length - offset} remains");
+ stringLength = (ushort)((entry.Data.Length - offset) / 2);
+ }
+
+ string stringValue = Encoding.Unicode.GetString(entry.Data, offset, stringLength * 2);
+ offset += stringLength * 2;
+ stringValue = stringValue.Replace("\n", "\\n").Replace("\r", newValue: "\\r");
+ stringTable[stringIndex++] = stringValue;
+ }
+ }
+
+ return stringTable;
+ }
+
+ ///
+ /// Read resource data as a version info resource
+ ///
+ /// Resource data entry to parse into a version info resource
+ /// A filled version info resource on success, null on error
+ public static Models.PortableExecutable.VersionInfo AsVersionInfo(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 versionInfo = new Models.PortableExecutable.VersionInfo();
+
+ versionInfo.Length = entry.Data.ReadUInt16(ref offset);
+ versionInfo.ValueLength = entry.Data.ReadUInt16(ref offset);
+ versionInfo.ResourceType = (Models.PortableExecutable.VersionResourceType)entry.Data.ReadUInt16(ref offset);
+ versionInfo.Key = entry.Data.ReadString(ref offset, Encoding.Unicode);
+ if (versionInfo.Key != "VS_VERSION_INFO")
+ return null;
+
+ while ((offset % 4) != 0)
+ versionInfo.Padding1 = entry.Data.ReadUInt16(ref offset);
+
+ // Read fixed file info
+ if (versionInfo.ValueLength > 0)
+ {
+ var fixedFileInfo = new Models.PortableExecutable.FixedFileInfo();
+ fixedFileInfo.Signature = entry.Data.ReadUInt32(ref offset);
+ if (fixedFileInfo.Signature != 0xFEEF04BD)
+ return null;
+
+ fixedFileInfo.StrucVersion = entry.Data.ReadUInt32(ref offset);
+ fixedFileInfo.FileVersionMS = entry.Data.ReadUInt32(ref offset);
+ fixedFileInfo.FileVersionLS = entry.Data.ReadUInt32(ref offset);
+ fixedFileInfo.ProductVersionMS = entry.Data.ReadUInt32(ref offset);
+ fixedFileInfo.ProductVersionLS = entry.Data.ReadUInt32(ref offset);
+ fixedFileInfo.FileFlagsMask = entry.Data.ReadUInt32(ref offset);
+ fixedFileInfo.FileFlags = (Models.PortableExecutable.FixedFileInfoFlags)(entry.Data.ReadUInt32(ref offset) & fixedFileInfo.FileFlagsMask);
+ fixedFileInfo.FileOS = (Models.PortableExecutable.FixedFileInfoOS)entry.Data.ReadUInt32(ref offset);
+ fixedFileInfo.FileType = (Models.PortableExecutable.FixedFileInfoFileType)entry.Data.ReadUInt32(ref offset);
+ fixedFileInfo.FileSubtype = (Models.PortableExecutable.FixedFileInfoFileSubtype)entry.Data.ReadUInt32(ref offset);
+ fixedFileInfo.FileDateMS = entry.Data.ReadUInt32(ref offset);
+ fixedFileInfo.FileDateLS = entry.Data.ReadUInt32(ref offset);
+ versionInfo.Value = fixedFileInfo;
+ }
+
+ while ((offset % 4) != 0)
+ versionInfo.Padding2 = entry.Data.ReadUInt16(ref offset);
+
+ // TODO: Make the following block a private helper method
+
+ // Determine if we have a StringFileInfo or VarFileInfo next
+ if (offset < versionInfo.Length)
+ {
+ // Cache the current offset for reading
+ int currentOffset = offset;
+
+ offset += 6;
+
+ string nextKey = entry.Data.ReadString(ref offset, Encoding.Unicode);
+ offset = currentOffset;
+
+ if (nextKey == "StringFileInfo")
+ {
+ var stringFileInfo = AsStringFileInfo(entry.Data, ref offset);
+ versionInfo.StringFileInfo = stringFileInfo;
+ }
+ else if (nextKey == "VarFileInfo")
+ {
+ var varFileInfo = AsVarFileInfo(entry.Data, ref offset);
+ versionInfo.VarFileInfo = varFileInfo;
+ }
+ }
+
+ // And again
+ if (offset < versionInfo.Length)
+ {
+ // Cache the current offset for reading
+ int currentOffset = offset;
+
+ offset += 6;
+ string nextKey = entry.Data.ReadString(ref offset, Encoding.Unicode);
+ offset = currentOffset;
+
+ if (nextKey == "StringFileInfo")
+ {
+ var stringFileInfo = AsStringFileInfo(entry.Data, ref offset);
+ versionInfo.StringFileInfo = stringFileInfo;
+ }
+ else if (nextKey == "VarFileInfo")
+ {
+ var varFileInfo = AsVarFileInfo(entry.Data, ref offset);
+ versionInfo.VarFileInfo = varFileInfo;
+ }
+ }
+
+ return versionInfo;
+ }
+
+ ///
+ /// Read byte data as a string file info resource
+ ///
+ /// Data to parse into a string file info
+ /// Offset into the byte array
+ /// A filled string file info resource on success, null on error
+ private static Models.PortableExecutable.StringFileInfo AsStringFileInfo(byte[] data, ref int offset)
+ {
+ var stringFileInfo = new Models.PortableExecutable.StringFileInfo();
+
+ // Cache the initial offset
+ int currentOffset = offset;
+
+ stringFileInfo.Length = data.ReadUInt16(ref offset);
+ stringFileInfo.ValueLength = data.ReadUInt16(ref offset);
+ stringFileInfo.ResourceType = (Models.PortableExecutable.VersionResourceType)data.ReadUInt16(ref offset);
+ stringFileInfo.Key = data.ReadString(ref offset, Encoding.Unicode);
+ if (stringFileInfo.Key != "StringFileInfo")
+ {
+ offset -= 6 + ((stringFileInfo.Key.Length + 1) * 2);
+ return null;
+ }
+
+ // Align to the DWORD boundary if we're not at the end
+ if (offset != data.Length)
+ {
+ while ((offset % 4) != 0)
+ stringFileInfo.Padding = data.ReadByte(ref offset);
+ }
+
+ var stringFileInfoChildren = new List();
+ while ((offset - currentOffset) < stringFileInfo.Length)
+ {
+ var stringTable = new Models.PortableExecutable.StringTable();
+
+ stringTable.Length = data.ReadUInt16(ref offset);
+ stringTable.ValueLength = data.ReadUInt16(ref offset);
+ stringTable.ResourceType = (Models.PortableExecutable.VersionResourceType)data.ReadUInt16(ref offset);
+ stringTable.Key = data.ReadString(ref offset, Encoding.Unicode);
+
+ // Align to the DWORD boundary if we're not at the end
+ if (offset != data.Length)
+ {
+ while ((offset % 4) != 0)
+ stringTable.Padding = data.ReadByte(ref offset);
+ }
+
+ var stringTableChildren = new List();
+ while ((offset - currentOffset) < stringTable.Length)
+ {
+ var stringData = new Models.PortableExecutable.StringData();
+
+ stringData.Length = data.ReadUInt16(ref offset);
+ stringData.ValueLength = data.ReadUInt16(ref offset);
+ stringData.ResourceType = (Models.PortableExecutable.VersionResourceType)data.ReadUInt16(ref offset);
+ stringData.Key = data.ReadString(ref offset, Encoding.Unicode);
+
+ // Align to the DWORD boundary if we're not at the end
+ if (offset != data.Length)
+ {
+ while ((offset % 4) != 0)
+ stringData.Padding = data.ReadByte(ref offset);
+ }
+
+ if (stringData.ValueLength > 0)
+ {
+ byte[] valueBytes = data.ReadBytes(ref offset, stringData.ValueLength * sizeof(ushort));
+ stringData.Value = Encoding.Unicode.GetString(valueBytes);
+ }
+
+ // Align to the DWORD boundary if we're not at the end
+ if (offset != data.Length)
+ {
+ while ((offset % 4) != 0)
+ _ = data.ReadByte(ref offset);
+ }
+
+ stringTableChildren.Add(stringData);
+ }
+
+ stringTable.Children = stringTableChildren.ToArray();
+
+ stringFileInfoChildren.Add(stringTable);
+ }
+
+ stringFileInfo.Children = stringFileInfoChildren.ToArray();
+
+ return stringFileInfo;
+ }
+
+ ///
+ /// Read byte data as a var file info resource
+ ///
+ /// Data to parse into a var file info
+ /// Offset into the byte array
+ /// A filled var file info resource on success, null on error
+ private static Models.PortableExecutable.VarFileInfo AsVarFileInfo(byte[] data, ref int offset)
+ {
+ var varFileInfo = new Models.PortableExecutable.VarFileInfo();
+
+ // Cache the initial offset
+ int initialOffset = offset;
+
+ varFileInfo.Length = data.ReadUInt16(ref offset);
+ varFileInfo.ValueLength = data.ReadUInt16(ref offset);
+ varFileInfo.ResourceType = (Models.PortableExecutable.VersionResourceType)data.ReadUInt16(ref offset);
+ varFileInfo.Key = data.ReadString(ref offset, Encoding.Unicode);
+ if (varFileInfo.Key != "VarFileInfo")
+ return null;
+
+ // Align to the DWORD boundary if we're not at the end
+ if (offset != data.Length)
+ {
+ while ((offset % 4) != 0)
+ varFileInfo.Padding = data.ReadByte(ref offset);
+ }
+
+ var varFileInfoChildren = new List();
+ while ((offset - initialOffset) < varFileInfo.Length)
+ {
+ var varData = new Models.PortableExecutable.VarData();
+
+ varData.Length = data.ReadUInt16(ref offset);
+ varData.ValueLength = data.ReadUInt16(ref offset);
+ varData.ResourceType = (Models.PortableExecutable.VersionResourceType)data.ReadUInt16(ref offset);
+ varData.Key = data.ReadString(ref offset, Encoding.Unicode);
+ if (varData.Key != "Translation")
+ {
+ offset -= 6 + ((varData.Key.Length + 1) * 2);
+ return null;
+ }
+
+ // Align to the DWORD boundary if we're not at the end
+ if (offset != data.Length)
+ {
+ while ((offset % 4) != 0)
+ varData.Padding = data.ReadByte(ref offset);
+ }
+
+ // Cache the current offset
+ int currentOffset = offset;
+
+ var varDataValue = new List();
+ while ((offset - currentOffset) < varData.ValueLength)
+ {
+ uint languageAndCodeIdentifierPair = data.ReadUInt32(ref offset);
+ varDataValue.Add(languageAndCodeIdentifierPair);
+ }
+
+ varData.Value = varDataValue.ToArray();
+
+ varFileInfoChildren.Add(varData);
+ }
+
+ varFileInfo.Children = varFileInfoChildren.ToArray();
+
+ return varFileInfo;
+ }
+
+ #endregion
+
+ #endregion
+ }
+}
\ No newline at end of file