using System.Collections.Generic; using System.IO; using System.Text; using SabreTools.Data.Extensions; using SabreTools.Data.Models.NewExecutable; using SabreTools.IO.Extensions; using SabreTools.Numerics.Extensions; using static SabreTools.Data.Models.NewExecutable.Constants; #pragma warning disable IDE0017 // Simplify object initialization namespace SabreTools.Serialization.Readers { public class NewExecutable : BaseBinaryReader { /// public override Executable? Deserialize(Stream? data) { // If the data is invalid if (data is null || !data.CanRead) return null; try { // Cache the current offset long initialOffset = data.Position; // Create a new executable to fill var nex = new Executable(); #region MS-DOS Stub // Parse the MS-DOS stub var stub = new MSDOS().Deserialize(data); if (stub?.Header is null || stub.Header.NewExeHeaderAddr == 0) return null; // Set the MS-DOS stub nex.Stub = stub; #endregion #region Executable Header // Get the new executable offset long newExeOffset = initialOffset + stub.Header.NewExeHeaderAddr; if (newExeOffset < initialOffset || newExeOffset > data.Length) return null; // Try to parse the executable header data.SeekIfPossible(newExeOffset, SeekOrigin.Begin); var header = ParseExecutableHeader(data); if (header.Magic != SignatureString) return null; // Set the executable header nex.Header = header; #endregion #region Segment Table // If the offset for the segment table doesn't exist long tableAddress = initialOffset + stub.Header.NewExeHeaderAddr + header.SegmentTableOffset; if (tableAddress >= data.Length) return nex; // Seek to the segment table data.SeekIfPossible(tableAddress, SeekOrigin.Begin); // Set the segment table nex.SegmentTable = new SegmentTableEntry[header.FileSegmentCount]; for (int i = 0; i < header.FileSegmentCount; i++) { nex.SegmentTable[i] = ParseSegmentTableEntry(data, initialOffset); } #endregion #region Resource Table // If the offset for the segment table doesn't exist tableAddress = initialOffset + stub.Header.NewExeHeaderAddr + header.ResourceTableOffset; if (tableAddress >= data.Length) return nex; // Seek to the resource table data.SeekIfPossible(tableAddress, SeekOrigin.Begin); // Set the resource table nex.ResourceTable = ParseResourceTable(data, header.ResourceEntriesCount); #endregion #region Resident-Name Table // If the offset for the resident-name table doesn't exist tableAddress = initialOffset + stub.Header.NewExeHeaderAddr + header.ResidentNameTableOffset; long endOffset = initialOffset + stub.Header.NewExeHeaderAddr + header.ModuleReferenceTableOffset; if (tableAddress >= data.Length) return nex; // Seek to the resident-name table data.SeekIfPossible(tableAddress, SeekOrigin.Begin); // Set the resident-name table nex.ResidentNameTable = ParseResidentNameTable(data, endOffset); #endregion #region Module-Reference Table // If the offset for the module-reference table doesn't exist tableAddress = initialOffset + stub.Header.NewExeHeaderAddr + header.ModuleReferenceTableOffset; if (tableAddress >= data.Length) return nex; // Seek to the module-reference table data.SeekIfPossible(tableAddress, SeekOrigin.Begin); // Set the module-reference table nex.ModuleReferenceTable = new ModuleReferenceTableEntry[header.ModuleReferenceTableSize]; for (int i = 0; i < header.ModuleReferenceTableSize; i++) { nex.ModuleReferenceTable[i] = ParseModuleReferenceTableEntry(data); } #endregion #region Imported-Name Table // If the offset for the imported-name table doesn't exist tableAddress = initialOffset + stub.Header.NewExeHeaderAddr + header.ImportedNamesTableOffset; endOffset = initialOffset + stub.Header.NewExeHeaderAddr + header.EntryTableOffset; if (tableAddress >= data.Length) return nex; // Seek to the imported-name table data.SeekIfPossible(tableAddress, SeekOrigin.Begin); // Set the imported-name table nex.ImportedNameTable = ParseImportedNameTable(data, endOffset); #endregion #region Entry Table // If the offset for the imported-name table doesn't exist tableAddress = initialOffset + stub.Header.NewExeHeaderAddr + header.EntryTableOffset; endOffset = initialOffset + stub.Header.NewExeHeaderAddr + header.EntryTableOffset + header.EntryTableSize; if (tableAddress >= data.Length) return nex; // Seek to the imported-name table data.SeekIfPossible(tableAddress, SeekOrigin.Begin); // Set the entry table nex.EntryTable = ParseEntryTable(data, endOffset); #endregion #region Nonresident-Name Table // If the offset for the nonresident-name table doesn't exist tableAddress = initialOffset + header.NonResidentNamesTableOffset; endOffset = initialOffset + header.NonResidentNamesTableOffset + header.NonResidentNameTableSize; if (tableAddress >= data.Length) return nex; // Seek to the nonresident-name table data.SeekIfPossible(tableAddress, SeekOrigin.Begin); // Set the nonresident-name table nex.NonResidentNameTable = ParseNonResidentNameTable(data, endOffset); #endregion return nex; } catch { // Ignore the actual error return null; } } /// /// Parse a Stream into an entry table /// /// Stream to parse /// First address not part of the entry table /// Filled entry table on success, null on error public static EntryTableBundle[] ParseEntryTable(Stream data, long endOffset) { var entryTable = new List(); while (data.Position < endOffset && data.Position < data.Length) { var entry = new EntryTableBundle(); entry.EntryCount = data.ReadByteValue(); entry.SegmentIndicator = data.ReadByteValue(); switch (entry.GetEntryType()) { case SegmentEntryType.Unused: break; case SegmentEntryType.FixedSegment: entry.FixedFlagWord = (FixedSegmentEntryFlag)data.ReadByteValue(); entry.FixedOffset = data.ReadUInt16LittleEndian(); break; case SegmentEntryType.MoveableSegment: entry.MoveableFlagWord = (MoveableSegmentEntryFlag)data.ReadByteValue(); entry.MoveableReserved = data.ReadUInt16LittleEndian(); entry.MoveableSegmentNumber = data.ReadByteValue(); entry.MoveableOffset = data.ReadUInt16LittleEndian(); break; default: // TODO: Log invalid values break; } entryTable.Add(entry); } return [.. entryTable]; } /// /// Parse a Stream into an ExecutableHeader /// /// Stream to parse /// Filled ExecutableHeader on success, null on error public static ExecutableHeader ParseExecutableHeader(Stream data) { var obj = new ExecutableHeader(); byte[] magic = data.ReadBytes(2); obj.Magic = Encoding.ASCII.GetString(magic); obj.LinkerVersion = data.ReadByteValue(); obj.LinkerRevision = data.ReadByteValue(); obj.EntryTableOffset = data.ReadUInt16LittleEndian(); obj.EntryTableSize = data.ReadUInt16LittleEndian(); obj.CrcChecksum = data.ReadUInt32LittleEndian(); obj.FlagWord = (HeaderFlag)data.ReadUInt16LittleEndian(); obj.AutomaticDataSegmentNumber = data.ReadUInt16LittleEndian(); obj.InitialHeapAlloc = data.ReadUInt16LittleEndian(); obj.InitialStackAlloc = data.ReadUInt16LittleEndian(); obj.InitialCSIPSetting = data.ReadUInt32LittleEndian(); obj.InitialSSSPSetting = data.ReadUInt32LittleEndian(); obj.FileSegmentCount = data.ReadUInt16LittleEndian(); obj.ModuleReferenceTableSize = data.ReadUInt16LittleEndian(); obj.NonResidentNameTableSize = data.ReadUInt16LittleEndian(); obj.SegmentTableOffset = data.ReadUInt16LittleEndian(); obj.ResourceTableOffset = data.ReadUInt16LittleEndian(); obj.ResidentNameTableOffset = data.ReadUInt16LittleEndian(); obj.ModuleReferenceTableOffset = data.ReadUInt16LittleEndian(); obj.ImportedNamesTableOffset = data.ReadUInt16LittleEndian(); obj.NonResidentNamesTableOffset = data.ReadUInt32LittleEndian(); obj.MovableEntriesCount = data.ReadUInt16LittleEndian(); obj.SegmentAlignmentShiftCount = data.ReadUInt16LittleEndian(); obj.ResourceEntriesCount = data.ReadUInt16LittleEndian(); obj.TargetOperatingSystem = (OperatingSystem)data.ReadByteValue(); obj.AdditionalFlags = (OS2Flag)data.ReadByteValue(); obj.ReturnThunkOffset = data.ReadUInt16LittleEndian(); obj.SegmentReferenceThunkOffset = data.ReadUInt16LittleEndian(); obj.MinCodeSwapAreaSize = data.ReadUInt16LittleEndian(); obj.WindowsSDKRevision = data.ReadByteValue(); obj.WindowsSDKVersion = data.ReadByteValue(); return obj; } /// /// Parse a Stream into an imported-name table /// /// Stream to parse /// First address not part of the imported-name table /// Filled imported-name table on success, null on error public static Dictionary ParseImportedNameTable(Stream data, long endOffset) { var obj = new Dictionary(); while (data.Position < endOffset && data.Position < data.Length) { ushort currentOffset = (ushort)data.Position; obj[currentOffset] = ParseImportedNameTableEntry(data); } return obj; } /// /// Parse a Stream into an ImportedNameTableEntry /// /// Stream to parse /// Filled ImportedNameTableEntry on success, null on error public static ImportedNameTableEntry ParseImportedNameTableEntry(Stream data) { var obj = new ImportedNameTableEntry(); obj.Length = data.ReadByteValue(); obj.NameString = data.ReadBytes(obj.Length); return obj; } /// /// Parse a Stream into an ImportOrdinalRelocationRecord /// /// Stream to parse /// Filled ImportOrdinalRelocationRecord on success, null on error public static ImportOrdinalRelocationRecord ParseImportOrdinalRelocationRecord(Stream data) { var obj = new ImportOrdinalRelocationRecord(); obj.Index = data.ReadUInt16LittleEndian(); obj.Ordinal = data.ReadUInt16LittleEndian(); return obj; } /// /// Parse a Stream into an InternalRefRelocationRecord /// /// Stream to parse /// Filled InternalRefRelocationRecord on success, null on error public static InternalRefRelocationRecord ParseInternalRefRelocationRecord(Stream data) { var obj = new InternalRefRelocationRecord(); obj.SegmentNumber = data.ReadByteValue(); obj.Reserved = data.ReadByteValue(); obj.Offset = data.ReadUInt16LittleEndian(); return obj; } /// /// Parse a Stream into an ModuleReferenceTableEntry /// /// Stream to parse /// Filled ModuleReferenceTableEntry on success, null on error public static ModuleReferenceTableEntry ParseModuleReferenceTableEntry(Stream data) { var obj = new ModuleReferenceTableEntry(); obj.Offset = data.ReadUInt16LittleEndian(); return obj; } /// /// Parse a Stream into an ImportNameRelocationRecord /// /// Stream to parse /// Filled ImportNameRelocationRecord on success, null on error public static ImportNameRelocationRecord ParseImportNameRelocationRecord(Stream data) { var obj = new ImportNameRelocationRecord(); obj.Index = data.ReadUInt16LittleEndian(); obj.Offset = data.ReadUInt16LittleEndian(); return obj; } /// /// Parse a Stream into a nonresident-name table /// /// Stream to parse /// First address not part of the nonresident-name table /// Filled nonresident-name table on success, null on error public static NonResidentNameTableEntry[] ParseNonResidentNameTable(Stream data, long endOffset) { var obj = new List(); while (data.Position < endOffset && data.Position < data.Length) { var entry = ParseNonResidentNameTableEntry(data); obj.Add(entry); } return [.. obj]; } /// /// Parse a Stream into a NonResidentNameTableEntry /// /// Stream to parse /// Filled NonResidentNameTableEntry on success, null on error public static NonResidentNameTableEntry ParseNonResidentNameTableEntry(Stream data) { var obj = new NonResidentNameTableEntry(); obj.Length = data.ReadByteValue(); obj.NameString = data.ReadBytes(obj.Length); obj.OrdinalNumber = data.ReadUInt16LittleEndian(); return obj; } /// /// Parse a Stream into an OSFixupRelocationRecord /// /// Stream to parse /// Filled OSFixupRelocationRecord on success, null on error public static OSFixupRelocationRecord ParseOSFixupRelocationRecord(Stream data) { var obj = new OSFixupRelocationRecord(); obj.FixupType = (OSFixupType)data.ReadUInt16LittleEndian(); obj.Reserved = data.ReadUInt16LittleEndian(); return obj; } /// /// Parse a Stream into an PerSegmentData /// /// Stream to parse /// Filled PerSegmentData on success, null on error public static PerSegmentData ParsePerSegmentData(Stream data) { var obj = new PerSegmentData(); obj.RelocationRecordCount = data.ReadUInt16LittleEndian(); obj.RelocationRecords = new RelocationRecord[obj.RelocationRecordCount]; for (int i = 0; i < obj.RelocationRecords.Length; i++) { if (data.Position >= data.Length) break; var record = ParseRelocationRecord(data); if (record is null) break; obj.RelocationRecords[i] = record; } return obj; } /// /// Parse a Stream into an RelocationRecord /// /// Stream to parse /// Filled RelocationRecord on success, null on error public static RelocationRecord? ParseRelocationRecord(Stream data) { // Handle partial relocation sections if (data.Position > data.Length - 4) return null; var obj = new RelocationRecord(); obj.SourceType = (RelocationRecordSourceType)data.ReadByteValue(); obj.Flags = (RelocationRecordFlag)data.ReadByteValue(); obj.Offset = data.ReadUInt16LittleEndian(); // Handle incomplete entries if (data.Position > data.Length - 4) return obj; switch (obj.Flags & RelocationRecordFlag.TARGET_MASK) { case RelocationRecordFlag.INTERNALREF: obj.InternalRefRelocationRecord = ParseInternalRefRelocationRecord(data); break; case RelocationRecordFlag.IMPORTORDINAL: obj.ImportOrdinalRelocationRecord = ParseImportOrdinalRelocationRecord(data); break; case RelocationRecordFlag.IMPORTNAME: obj.ImportNameRelocationRecord = ParseImportNameRelocationRecord(data); break; case RelocationRecordFlag.OSFIXUP: obj.OSFixupRelocationRecord = ParseOSFixupRelocationRecord(data); break; case RelocationRecordFlag.ADDITIVE: // TODO: Figure out the record for this, if possible break; default: // TODO: Log invalid values break; } return obj; } /// /// Parse a Stream into a resident-name table /// /// Stream to parse /// First address not part of the resident-name table /// Filled resident-name table on success, null on error public static ResidentNameTableEntry[] ParseResidentNameTable(Stream data, long endOffset) { var obj = new List(); while (data.Position < endOffset && data.Position < data.Length) { var entry = ParseResidentNameTableEntry(data); obj.Add(entry); } return [.. obj]; } /// /// Parse a Stream into a ResidentNameTableEntry /// /// Stream to parse /// Filled ResidentNameTableEntry on success, null on error public static ResidentNameTableEntry ParseResidentNameTableEntry(Stream data) { var obj = new ResidentNameTableEntry(); obj.Length = data.ReadByteValue(); obj.NameString = data.ReadBytes(obj.Length); obj.OrdinalNumber = data.ReadUInt16LittleEndian(); return obj; } /// /// Parse a Stream into a ResourceTable /// /// Stream to parse /// Number of resource table entries to read /// Filled ResourceTable on success, null on error public static ResourceTable ParseResourceTable(Stream data, ushort count) { long initialOffset = data.Position; var resourceTable = new ResourceTable(); resourceTable.AlignmentShiftCount = data.ReadUInt16LittleEndian(); var resourceTypes = new List(); for (int i = 0; i < count; i++) { var entry = ParseResourceTypeInformationEntry(data); resourceTypes.Add(entry); // A zero type ID marks the end of the resource type information blocks. if (entry.TypeID == 0) break; } resourceTable.ResourceTypes = [.. resourceTypes]; // Get the full list of unique string offsets var stringOffsets = new List(); foreach (var rtie in resourceTable.ResourceTypes) { // Skip invalid entries if (rtie is null || rtie.TypeID == 0) continue; // Handle offset types if (!rtie.IsIntegerType() && !stringOffsets.Contains(rtie.TypeID)) stringOffsets.Add(rtie.TypeID); // Handle types with resources foreach (var rtre in rtie.Resources) { // Skip invalid entries if (rtre is null || rtre.IsIntegerType() || rtre.ResourceID == 0) continue; // Skip already added entries if (stringOffsets.Contains(rtre.ResourceID)) continue; stringOffsets.Add(rtre.ResourceID); } } // Order the offsets list stringOffsets.Sort(); // Populate the type and name string dictionary resourceTable.TypeAndNameStrings = []; for (int i = 0; i < stringOffsets.Count; i++) { int stringOffset = (int)(stringOffsets[i] + initialOffset); data.SeekIfPossible(stringOffset, SeekOrigin.Begin); var str = ParseResourceTypeAndNameString(data); resourceTable.TypeAndNameStrings[stringOffsets[i]] = str; } return resourceTable; } /// /// Parse a Stream into an ResourceTypeInformationEntry /// /// Stream to parse /// Filled ResourceTypeInformationEntry on success, null on error public static ResourceTypeInformationEntry ParseResourceTypeInformationEntry(Stream data) { var obj = new ResourceTypeInformationEntry(); obj.TypeID = data.ReadUInt16LittleEndian(); obj.ResourceCount = data.ReadUInt16LittleEndian(); obj.Reserved = data.ReadUInt32LittleEndian(); // A zero type ID marks the end of the resource type information blocks. if (obj.TypeID == 0) return obj; obj.Resources = new ResourceTypeResourceEntry[obj.ResourceCount]; for (int i = 0; i < obj.ResourceCount; i++) { // TODO: Should we read and store the resource data? obj.Resources[i] = ParseResourceTypeResourceEntry(data); } return obj; } /// /// Parse a Stream into a ResourceTypeAndNameString /// /// Stream to parse /// Filled rResourceTypeAndNameString on success, null on error public static ResourceTypeAndNameString ParseResourceTypeAndNameString(Stream data) { var obj = new ResourceTypeAndNameString(); obj.Length = data.ReadByteValue(); obj.Text = data.ReadBytes(obj.Length); return obj; } /// /// Parse a Stream into an ResourceTypeResourceEntry /// /// Stream to parse /// Filled ResourceTypeResourceEntry on success, null on error public static ResourceTypeResourceEntry ParseResourceTypeResourceEntry(Stream data) { var obj = new ResourceTypeResourceEntry(); obj.Offset = data.ReadUInt16LittleEndian(); obj.Length = data.ReadUInt16LittleEndian(); obj.FlagWord = (ResourceTypeResourceFlag)data.ReadUInt16LittleEndian(); obj.ResourceID = data.ReadUInt16LittleEndian(); obj.Reserved = data.ReadUInt32LittleEndian(); return obj; } /// /// Parse a Stream into an SegmentTableEntry /// /// Stream to parse /// Initial offset to use in address comparisons /// Filled SegmentTableEntry on success, null on error public static SegmentTableEntry ParseSegmentTableEntry(Stream data, long initialOffset) { var obj = new SegmentTableEntry(); obj.Offset = data.ReadUInt16LittleEndian(); obj.Length = data.ReadUInt16LittleEndian(); obj.FlagWord = (SegmentTableEntryFlag)data.ReadUInt16LittleEndian(); obj.MinimumAllocationSize = data.ReadUInt16LittleEndian(); // If the data offset is invalid if (obj.Offset < 0 || obj.Offset + initialOffset >= data.Length) return obj; // Cache the current offset long currentOffset = data.Position; // Seek to the data offset and read data.SeekIfPossible(obj.Offset + initialOffset, SeekOrigin.Begin); obj.Data = data.ReadBytes(obj.Length); #if NET20 || NET35 if ((obj.FlagWord & SegmentTableEntryFlag.RELOCINFO) != 0) #else if (obj.FlagWord.HasFlag(flag: SegmentTableEntryFlag.RELOCINFO)) #endif { obj.PerSegmentData = ParsePerSegmentData(data); } // Seek back to the end of the entry data.SeekIfPossible(currentOffset, SeekOrigin.Begin); return obj; } } }