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