Files
SabreTools.Serialization/SabreTools.Serialization/Readers/NewExecutable.cs
2026-01-27 12:03:01 -05:00

697 lines
27 KiB
C#

using System.Collections.Generic;
using System.IO;
using System.Text;
using SabreTools.Data.Extensions;
using SabreTools.Data.Models.NewExecutable;
using SabreTools.IO.Extensions;
using static SabreTools.Data.Models.NewExecutable.Constants;
#pragma warning disable IDE0017 // Simplify object initialization
namespace SabreTools.Serialization.Readers
{
public class NewExecutable : BaseBinaryReader<Executable>
{
/// <inheritdoc/>
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;
}
}
/// <summary>
/// Parse a Stream into an entry table
/// </summary>
/// <param name="data">Stream to parse</param>
/// <param name="endOffset">First address not part of the entry table</param>
/// <returns>Filled entry table on success, null on error</returns>
public static EntryTableBundle[] ParseEntryTable(Stream data, long endOffset)
{
var entryTable = new List<EntryTableBundle>();
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];
}
/// <summary>
/// Parse a Stream into an ExecutableHeader
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled ExecutableHeader on success, null on error</returns>
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;
}
/// <summary>
/// Parse a Stream into an imported-name table
/// </summary>
/// <param name="data">Stream to parse</param>
/// <param name="endOffset">First address not part of the imported-name table</param>
/// <returns>Filled imported-name table on success, null on error</returns>
public static Dictionary<ushort, ImportedNameTableEntry> ParseImportedNameTable(Stream data, long endOffset)
{
var obj = new Dictionary<ushort, ImportedNameTableEntry>();
while (data.Position < endOffset && data.Position < data.Length)
{
ushort currentOffset = (ushort)data.Position;
obj[currentOffset] = ParseImportedNameTableEntry(data);
}
return obj;
}
/// <summary>
/// Parse a Stream into an ImportedNameTableEntry
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled ImportedNameTableEntry on success, null on error</returns>
public static ImportedNameTableEntry ParseImportedNameTableEntry(Stream data)
{
var obj = new ImportedNameTableEntry();
obj.Length = data.ReadByteValue();
obj.NameString = data.ReadBytes(obj.Length);
return obj;
}
/// <summary>
/// Parse a Stream into an ImportOrdinalRelocationRecord
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled ImportOrdinalRelocationRecord on success, null on error</returns>
public static ImportOrdinalRelocationRecord ParseImportOrdinalRelocationRecord(Stream data)
{
var obj = new ImportOrdinalRelocationRecord();
obj.Index = data.ReadUInt16LittleEndian();
obj.Ordinal = data.ReadUInt16LittleEndian();
return obj;
}
/// <summary>
/// Parse a Stream into an InternalRefRelocationRecord
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled InternalRefRelocationRecord on success, null on error</returns>
public static InternalRefRelocationRecord ParseInternalRefRelocationRecord(Stream data)
{
var obj = new InternalRefRelocationRecord();
obj.SegmentNumber = data.ReadByteValue();
obj.Reserved = data.ReadByteValue();
obj.Offset = data.ReadUInt16LittleEndian();
return obj;
}
/// <summary>
/// Parse a Stream into an ModuleReferenceTableEntry
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled ModuleReferenceTableEntry on success, null on error</returns>
public static ModuleReferenceTableEntry ParseModuleReferenceTableEntry(Stream data)
{
var obj = new ModuleReferenceTableEntry();
obj.Offset = data.ReadUInt16LittleEndian();
return obj;
}
/// <summary>
/// Parse a Stream into an ImportNameRelocationRecord
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled ImportNameRelocationRecord on success, null on error</returns>
public static ImportNameRelocationRecord ParseImportNameRelocationRecord(Stream data)
{
var obj = new ImportNameRelocationRecord();
obj.Index = data.ReadUInt16LittleEndian();
obj.Offset = data.ReadUInt16LittleEndian();
return obj;
}
/// <summary>
/// Parse a Stream into a nonresident-name table
/// </summary>
/// <param name="data">Stream to parse</param>
/// <param name="endOffset">First address not part of the nonresident-name table</param>
/// <returns>Filled nonresident-name table on success, null on error</returns>
public static NonResidentNameTableEntry[] ParseNonResidentNameTable(Stream data, long endOffset)
{
var obj = new List<NonResidentNameTableEntry>();
while (data.Position < endOffset && data.Position < data.Length)
{
var entry = ParseNonResidentNameTableEntry(data);
obj.Add(entry);
}
return [.. obj];
}
/// <summary>
/// Parse a Stream into a NonResidentNameTableEntry
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled NonResidentNameTableEntry on success, null on error</returns>
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;
}
/// <summary>
/// Parse a Stream into an OSFixupRelocationRecord
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled OSFixupRelocationRecord on success, null on error</returns>
public static OSFixupRelocationRecord ParseOSFixupRelocationRecord(Stream data)
{
var obj = new OSFixupRelocationRecord();
obj.FixupType = (OSFixupType)data.ReadUInt16LittleEndian();
obj.Reserved = data.ReadUInt16LittleEndian();
return obj;
}
/// <summary>
/// Parse a Stream into an PerSegmentData
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled PerSegmentData on success, null on error</returns>
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;
}
/// <summary>
/// Parse a Stream into an RelocationRecord
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled RelocationRecord on success, null on error</returns>
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;
}
/// <summary>
/// Parse a Stream into a resident-name table
/// </summary>
/// <param name="data">Stream to parse</param>
/// <param name="endOffset">First address not part of the resident-name table</param>
/// <returns>Filled resident-name table on success, null on error</returns>
public static ResidentNameTableEntry[] ParseResidentNameTable(Stream data, long endOffset)
{
var obj = new List<ResidentNameTableEntry>();
while (data.Position < endOffset && data.Position < data.Length)
{
var entry = ParseResidentNameTableEntry(data);
obj.Add(entry);
}
return [.. obj];
}
/// <summary>
/// Parse a Stream into a ResidentNameTableEntry
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled ResidentNameTableEntry on success, null on error</returns>
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;
}
/// <summary>
/// Parse a Stream into a ResourceTable
/// </summary>
/// <param name="data">Stream to parse</param>
/// <param name="count">Number of resource table entries to read</param>
/// <returns>Filled ResourceTable on success, null on error</returns>
public static ResourceTable ParseResourceTable(Stream data, ushort count)
{
long initialOffset = data.Position;
var resourceTable = new ResourceTable();
resourceTable.AlignmentShiftCount = data.ReadUInt16LittleEndian();
var resourceTypes = new List<ResourceTypeInformationEntry>();
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<ushort>();
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;
}
/// <summary>
/// Parse a Stream into an ResourceTypeInformationEntry
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled ResourceTypeInformationEntry on success, null on error</returns>
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;
}
/// <summary>
/// Parse a Stream into a ResourceTypeAndNameString
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled rResourceTypeAndNameString on success, null on error</returns>
public static ResourceTypeAndNameString ParseResourceTypeAndNameString(Stream data)
{
var obj = new ResourceTypeAndNameString();
obj.Length = data.ReadByteValue();
obj.Text = data.ReadBytes(obj.Length);
return obj;
}
/// <summary>
/// Parse a Stream into an ResourceTypeResourceEntry
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled ResourceTypeResourceEntry on success, null on error</returns>
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;
}
/// <summary>
/// Parse a Stream into an SegmentTableEntry
/// </summary>
/// <param name="data">Stream to parse</param>
/// <param name="initialOffset">Initial offset to use in address comparisons</param>
/// <returns>Filled SegmentTableEntry on success, null on error</returns>
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;
}
}
}