mirror of
https://github.com/SabreTools/SabreTools.Serialization.git
synced 2026-04-05 22:01:33 +00:00
644 lines
22 KiB
C#
644 lines
22 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using SabreTools.Data.Models.NewExecutable;
|
|
using SabreTools.IO.Extensions;
|
|
using SabreTools.Text.Extensions;
|
|
|
|
#pragma warning disable IDE0330 // Use 'System.Threading.Lock'
|
|
namespace SabreTools.Wrappers
|
|
{
|
|
public partial class NewExecutable : WrapperBase<Executable>
|
|
{
|
|
#region Descriptive Properties
|
|
|
|
/// <inheritdoc/>
|
|
public override string DescriptionString => "New Executable (NE)";
|
|
|
|
#endregion
|
|
|
|
#region Extension Properties
|
|
|
|
/// <inheritdoc cref="Executable.Header"/>
|
|
public ExecutableHeader Header => Model.Header;
|
|
|
|
/// <inheritdoc cref="Executable.ImportedNameTable"/>
|
|
public Dictionary<ushort, ImportedNameTableEntry> ImportedNameTable => Model.ImportedNameTable;
|
|
|
|
/// <inheritdoc cref="Executable.NonResidentNameTable"/>
|
|
public NonResidentNameTableEntry[] NonResidentNameTable => Model.NonResidentNameTable;
|
|
|
|
/// <summary>
|
|
/// Address of the overlay, if it exists
|
|
/// </summary>
|
|
/// <see href="https://codeberg.org/CYBERDEV/REWise/src/branch/master/src/exefile.c"/>
|
|
public long OverlayAddress
|
|
{
|
|
get
|
|
{
|
|
lock (_overlayAddressLock)
|
|
{
|
|
// Use the cached data if possible
|
|
if (field >= 0)
|
|
return field;
|
|
|
|
// Get the available source length, if possible
|
|
long dataLength = Length;
|
|
if (dataLength == -1)
|
|
{
|
|
field = -1;
|
|
return field;
|
|
}
|
|
|
|
// Search through the segments table to find the furthest
|
|
long endOfSectionData = -1;
|
|
foreach (var entry in SegmentTable)
|
|
{
|
|
// Get end of segment data
|
|
long offset = (entry.Offset * (1 << Header.SegmentAlignmentShiftCount)) + entry.Length;
|
|
|
|
// Read and find the end of the relocation data
|
|
#if NET20 || NET35
|
|
if ((entry.FlagWord & SegmentTableEntryFlag.RELOCINFO) != 0)
|
|
#else
|
|
if (entry.FlagWord.HasFlag(SegmentTableEntryFlag.RELOCINFO))
|
|
#endif
|
|
{
|
|
lock (_dataSourceLock)
|
|
{
|
|
_dataSource.SeekIfPossible(offset, SeekOrigin.Begin);
|
|
var relocationData = Serialization.Readers.NewExecutable.ParsePerSegmentData(_dataSource);
|
|
|
|
offset = _dataSource.Position;
|
|
}
|
|
}
|
|
|
|
if (offset > endOfSectionData)
|
|
endOfSectionData = offset;
|
|
}
|
|
|
|
// Search through the resources table to find the furthest
|
|
foreach (var entry in ResourceTable.ResourceTypes)
|
|
{
|
|
// Skip invalid entries
|
|
if (entry.ResourceCount == 0 || entry.Resources.Length == 0)
|
|
continue;
|
|
|
|
foreach (var resource in entry.Resources)
|
|
{
|
|
int offset = (resource.Offset << ResourceTable.AlignmentShiftCount) + resource.Length;
|
|
if (offset > endOfSectionData)
|
|
endOfSectionData = offset;
|
|
}
|
|
}
|
|
|
|
// If we didn't find the end of section data
|
|
if (endOfSectionData <= 0)
|
|
endOfSectionData = -1;
|
|
|
|
// Adjust the position of the data by 705 bytes
|
|
// TODO: Investigate what the byte data is
|
|
endOfSectionData += 705;
|
|
|
|
// Cache and return the position
|
|
field = endOfSectionData;
|
|
return field;
|
|
}
|
|
}
|
|
} = -1;
|
|
|
|
/// <summary>
|
|
/// Overlay data, if it exists
|
|
/// </summary>
|
|
/// <remarks>Caches up to 0x10000 bytes</remarks>
|
|
/// <see href="https://codeberg.org/CYBERDEV/REWise/src/branch/master/src/exefile.c"/>
|
|
public byte[] OverlayData
|
|
{
|
|
get
|
|
{
|
|
lock (_overlayDataLock)
|
|
{
|
|
// Use the cached data if possible
|
|
if (field is not null)
|
|
return field;
|
|
|
|
// Get the available source length, if possible
|
|
long dataLength = Length;
|
|
if (dataLength == -1)
|
|
{
|
|
field = [];
|
|
return field;
|
|
}
|
|
|
|
// Get the overlay address and size if possible
|
|
long endOfSectionData = OverlayAddress;
|
|
long overlaySize = OverlaySize;
|
|
|
|
// If we didn't find the address or size
|
|
if (endOfSectionData <= 0 || overlaySize <= 0)
|
|
{
|
|
field = [];
|
|
return field;
|
|
}
|
|
|
|
// If we're at the end of the file, cache an empty byte array
|
|
if (endOfSectionData >= dataLength)
|
|
{
|
|
field = [];
|
|
return field;
|
|
}
|
|
|
|
// Otherwise, cache and return the data
|
|
overlaySize = Math.Min(overlaySize, 0x10000);
|
|
|
|
field = ReadRangeFromSource((int)endOfSectionData, (int)overlaySize);
|
|
return field;
|
|
}
|
|
}
|
|
} = null;
|
|
|
|
/// <summary>
|
|
/// Size of the overlay data, if it exists
|
|
/// </summary>
|
|
/// <see href="https://codeberg.org/CYBERDEV/REWise/src/branch/master/src/exefile.c"/>
|
|
public long OverlaySize
|
|
{
|
|
get
|
|
{
|
|
lock (_overlaySizeLock)
|
|
{
|
|
// Use the cached data if possible
|
|
if (field >= 0)
|
|
return field;
|
|
|
|
// Get the available source length, if possible
|
|
long dataLength = Length;
|
|
if (dataLength == -1)
|
|
{
|
|
field = 0;
|
|
return field;
|
|
}
|
|
|
|
// Get the overlay address if possible
|
|
long endOfSectionData = OverlayAddress;
|
|
|
|
// If we didn't find the end of section data
|
|
if (endOfSectionData <= 0)
|
|
{
|
|
field = 0;
|
|
return field;
|
|
}
|
|
|
|
// If we're at the end of the file, cache an empty byte array
|
|
if (endOfSectionData >= dataLength)
|
|
{
|
|
field = 0;
|
|
return field;
|
|
}
|
|
|
|
// Otherwise, cache and return the data
|
|
field = dataLength - endOfSectionData;
|
|
return field;
|
|
}
|
|
}
|
|
} = -1;
|
|
|
|
/// <summary>
|
|
/// Overlay strings, if they exist
|
|
/// </summary>
|
|
public List<string> OverlayStrings
|
|
{
|
|
get
|
|
{
|
|
lock (_overlayStringsLock)
|
|
{
|
|
// Use the cached data if possible
|
|
if (field is not null)
|
|
return field;
|
|
|
|
// Get the available source length, if possible
|
|
long dataLength = Length;
|
|
if (dataLength == -1)
|
|
{
|
|
field = [];
|
|
return field;
|
|
}
|
|
|
|
// Get the overlay data, if possible
|
|
var overlayData = OverlayData;
|
|
if (overlayData.Length == 0)
|
|
{
|
|
field = [];
|
|
return field;
|
|
}
|
|
|
|
// Otherwise, cache and return the strings
|
|
field = overlayData.ReadStringsFrom(charLimit: 3) ?? [];
|
|
return field;
|
|
}
|
|
}
|
|
} = null;
|
|
|
|
/// <inheritdoc cref="Executable.ResidentNameTable"/>
|
|
public ResidentNameTableEntry[] ResidentNameTable => Model.ResidentNameTable;
|
|
|
|
/// <inheritdoc cref="Executable.ResourceTable"/>
|
|
public ResourceTable ResourceTable => Model.ResourceTable;
|
|
|
|
/// <inheritdoc cref="Executable.SegmentTable"/>
|
|
public SegmentTableEntry[] SegmentTable => Model.SegmentTable;
|
|
|
|
/// <inheritdoc cref="Executable.Stub"/>
|
|
public Data.Models.MSDOS.Executable Stub => Model.Stub;
|
|
|
|
/// <summary>
|
|
/// Stub executable data, if it exists
|
|
/// </summary>
|
|
public byte[] StubExecutableData
|
|
{
|
|
get
|
|
{
|
|
lock (_stubExecutableDataLock)
|
|
{
|
|
// If we already have cached data, just use that immediately
|
|
if (field is not null)
|
|
return field;
|
|
|
|
// Populate the raw stub executable data based on the source
|
|
int endOfStubHeader = 0x40;
|
|
int lengthOfStubExecutableData = (int)Stub.Header.NewExeHeaderAddr - endOfStubHeader;
|
|
field = ReadRangeFromSource(endOfStubHeader, lengthOfStubExecutableData);
|
|
|
|
// Cache and return the stub executable data, even if null
|
|
return field;
|
|
}
|
|
}
|
|
} = null;
|
|
|
|
#endregion
|
|
|
|
#region Instance Variables
|
|
|
|
/// <summary>
|
|
/// Lock object for <see cref="OverlayAddress"/>
|
|
/// </summary>
|
|
private readonly object _overlayAddressLock = new();
|
|
|
|
/// <summary>
|
|
/// Lock object for <see cref="OverlayData"/>
|
|
/// </summary>
|
|
private readonly object _overlayDataLock = new();
|
|
|
|
/// <summary>
|
|
/// Lock object for <see cref="OverlaySize"/>
|
|
/// </summary>
|
|
private readonly object _overlaySizeLock = new();
|
|
|
|
/// <summary>
|
|
/// Lock object for <see cref="OverlayStrings"/>
|
|
/// </summary>
|
|
private readonly object _overlayStringsLock = new();
|
|
|
|
/// <summary>
|
|
/// Lock object for <see cref="StubExecutableData"/>
|
|
/// </summary>
|
|
private readonly object _stubExecutableDataLock = new();
|
|
|
|
#endregion
|
|
|
|
#region Constructors
|
|
|
|
/// <inheritdoc/>
|
|
public NewExecutable(Executable model, byte[] data) : base(model, data) { }
|
|
|
|
/// <inheritdoc/>
|
|
public NewExecutable(Executable model, byte[] data, int offset) : base(model, data, offset) { }
|
|
|
|
/// <inheritdoc/>
|
|
public NewExecutable(Executable model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
|
|
|
|
/// <inheritdoc/>
|
|
public NewExecutable(Executable model, Stream data) : base(model, data) { }
|
|
|
|
/// <inheritdoc/>
|
|
public NewExecutable(Executable model, Stream data, long offset) : base(model, data, offset) { }
|
|
|
|
/// <inheritdoc/>
|
|
public NewExecutable(Executable model, Stream data, long offset, long length) : base(model, data, offset, length) { }
|
|
|
|
#endregion
|
|
|
|
#region Static Constructors
|
|
|
|
/// <summary>
|
|
/// Create an NE executable from a byte array and offset
|
|
/// </summary>
|
|
/// <param name="data">Byte array representing the executable</param>
|
|
/// <param name="offset">Offset within the array to parse</param>
|
|
/// <returns>An NE executable wrapper on success, null on failure</returns>
|
|
public static NewExecutable? Create(byte[]? data, int offset)
|
|
{
|
|
// If the data is invalid
|
|
if (data is null || data.Length == 0)
|
|
return null;
|
|
|
|
// If the offset is out of bounds
|
|
if (offset < 0 || offset >= data.Length)
|
|
return null;
|
|
|
|
// Create a memory stream and use that
|
|
var dataStream = new MemoryStream(data, offset, data.Length - offset);
|
|
return Create(dataStream);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create an NE executable from a Stream
|
|
/// </summary>
|
|
/// <param name="data">Stream representing the executable</param>
|
|
/// <returns>An NE executable wrapper on success, null on failure</returns>
|
|
public static NewExecutable? Create(Stream? data)
|
|
{
|
|
// If the data is invalid
|
|
if (data is null || !data.CanRead)
|
|
return null;
|
|
|
|
try
|
|
{
|
|
// Cache the current offset
|
|
long currentOffset = data.Position;
|
|
|
|
var model = new Serialization.Readers.NewExecutable().Deserialize(data);
|
|
if (model is null)
|
|
return null;
|
|
|
|
return new NewExecutable(model, data, currentOffset);
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Resources
|
|
|
|
/// <summary>
|
|
/// Find the location of a Wise overlay header, if it exists
|
|
/// </summary>
|
|
/// <param name="includeDebug">True to include debug data, false otherwise</param>
|
|
/// <returns>Offset to the overlay header on success, -1 otherwise</returns>
|
|
public long FindWiseOverlayHeader()
|
|
{
|
|
// Get the overlay offset
|
|
long overlayOffset = OverlayAddress;
|
|
if (overlayOffset < 0 || overlayOffset >= Length)
|
|
return -1;
|
|
|
|
lock (_dataSourceLock)
|
|
{
|
|
// Attempt to get the overlay header
|
|
_dataSource.SeekIfPossible(overlayOffset, SeekOrigin.Begin);
|
|
var header = WiseOverlayHeader.Create(_dataSource);
|
|
if (header is not null)
|
|
return overlayOffset;
|
|
|
|
// Align and loop to see if it can be found
|
|
_dataSource.SeekIfPossible(overlayOffset, SeekOrigin.Begin);
|
|
_dataSource.AlignToBoundary(0x10);
|
|
overlayOffset = _dataSource.Position;
|
|
while (_dataSource.Position < Length)
|
|
{
|
|
_dataSource.SeekIfPossible(overlayOffset, SeekOrigin.Begin);
|
|
header = WiseOverlayHeader.Create(_dataSource);
|
|
if (header is not null)
|
|
return overlayOffset;
|
|
|
|
overlayOffset += 0x10;
|
|
}
|
|
|
|
header = null;
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get a single resource entry
|
|
/// </summary>
|
|
/// <param name="id">Resource ID to retrieve</param>
|
|
/// <returns>Resource on success, null otherwise</returns>
|
|
public ResourceTypeResourceEntry? GetResource(int id)
|
|
{
|
|
// Get the available source length, if possible
|
|
long dataLength = Length;
|
|
if (dataLength == -1)
|
|
return null;
|
|
|
|
// If the resource table is invalid
|
|
if (ResourceTable.ResourceTypes.Length == 0)
|
|
return null;
|
|
|
|
// Loop through the resources to find a matching ID
|
|
foreach (var resourceType in ResourceTable.ResourceTypes)
|
|
{
|
|
// Skip invalid resource types
|
|
if (resourceType.ResourceCount == 0 || resourceType.Resources.Length == 0)
|
|
continue;
|
|
|
|
// Loop through the entries to find a matching ID
|
|
foreach (var resource in resourceType.Resources)
|
|
{
|
|
// Skip non-matching entries
|
|
if (resource.ResourceID != id)
|
|
continue;
|
|
|
|
// Return the resource
|
|
return resource;
|
|
}
|
|
}
|
|
|
|
// No entry could be found
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the data for a single resource entry
|
|
/// </summary>
|
|
/// <param name="id">Resource ID to retrieve</param>
|
|
/// <returns>Resource data on success, null otherwise</returns>
|
|
public byte[]? GetResourceData(int id)
|
|
{
|
|
// Get the resource offset
|
|
int offset = GetResourceOffset(id);
|
|
if (offset < 0)
|
|
return null;
|
|
|
|
// Get the resource length
|
|
int length = GetResourceLength(id);
|
|
if (length < 0)
|
|
return null;
|
|
else if (length == 0)
|
|
return [];
|
|
|
|
// Read the resource data and return
|
|
return ReadRangeFromSource(offset, length);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the data length for a single resource entry
|
|
/// </summary>
|
|
/// <param name="id">Resource ID to retrieve</param>
|
|
/// <returns>Resource length on success, -1 otherwise</returns>
|
|
public int GetResourceLength(int id)
|
|
{
|
|
// Get the matching resource
|
|
var resource = GetSegment(id);
|
|
if (resource is null)
|
|
return -1;
|
|
|
|
// Return the reported length
|
|
return resource.Length;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the data offset for a single resource entry
|
|
/// </summary>
|
|
/// <param name="id">Resource ID to retrieve</param>
|
|
/// <returns>Resource offset on success, -1 otherwise</returns>
|
|
public int GetResourceOffset(int id)
|
|
{
|
|
// Get the available source length, if possible
|
|
long dataLength = Length;
|
|
if (dataLength == -1)
|
|
return -1;
|
|
|
|
// Get the matching resource
|
|
var resource = GetSegment(id);
|
|
if (resource is null)
|
|
return -1;
|
|
|
|
// Verify the resource offset
|
|
int offset = resource.Offset << ResourceTable.AlignmentShiftCount;
|
|
if (offset < 0 || offset + resource.Length >= dataLength)
|
|
return -1;
|
|
|
|
// Return the verified offset
|
|
return offset;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Segments
|
|
|
|
/// <summary>
|
|
/// Get a single segment
|
|
/// </summary>
|
|
/// <param name="index">Segment index to retrieve</param>
|
|
/// <returns>Segment on success, null otherwise</returns>
|
|
public SegmentTableEntry? GetSegment(int index)
|
|
{
|
|
// If the segment table is invalid
|
|
if (SegmentTable.Length == 0)
|
|
return null;
|
|
|
|
// If the index is invalid
|
|
if (index < 0 || index >= SegmentTable.Length)
|
|
return null;
|
|
|
|
// Return the segment
|
|
return SegmentTable[index];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the data for a single segment
|
|
/// </summary>
|
|
/// <param name="index">Segment index to retrieve</param>
|
|
/// <returns>Segment data on success, null otherwise</returns>
|
|
public byte[]? GetSegmentData(int index)
|
|
{
|
|
// Get the segment offset
|
|
int offset = GetSegmentOffset(index);
|
|
if (offset < 0)
|
|
return null;
|
|
|
|
// Get the segment length
|
|
int length = GetSegmentLength(index);
|
|
if (length < 0)
|
|
return null;
|
|
else if (length == 0)
|
|
return [];
|
|
|
|
// Read the segment data and return
|
|
return ReadRangeFromSource(offset, length);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the data length for a single segment
|
|
/// </summary>
|
|
/// <param name="index">Segment index to retrieve</param>
|
|
/// <returns>Segment length on success, -1 otherwise</returns>
|
|
public int GetSegmentLength(int index)
|
|
{
|
|
// Get the matching segment
|
|
var segment = GetSegment(index);
|
|
if (segment is null)
|
|
return -1;
|
|
|
|
// Return the reported length
|
|
return segment.Length;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the data offset for a single segment
|
|
/// </summary>
|
|
/// <param name="index">Segment index to retrieve</param>
|
|
/// <returns>Segment offset on success, -1 otherwise</returns>
|
|
public int GetSegmentOffset(int index)
|
|
{
|
|
// Get the available source length, if possible
|
|
long dataLength = Length;
|
|
if (dataLength == -1)
|
|
return -1;
|
|
|
|
// Get the matching segment
|
|
var segment = GetSegment(index);
|
|
if (segment is null)
|
|
return -1;
|
|
|
|
// Verify the segment offset
|
|
int offset = segment.Offset << Header.SegmentAlignmentShiftCount;
|
|
if (offset < 0 || offset + segment.Length >= dataLength)
|
|
return -1;
|
|
|
|
// Return the verified offset
|
|
return offset;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region REMOVE -- DO NOT USE
|
|
|
|
/// <summary>
|
|
/// Read an arbitrary range from the source
|
|
/// </summary>
|
|
/// <param name="rangeStart">The start of where to read data from, -1 means start of source</param>
|
|
/// <param name="length">How many bytes to read, -1 means read until end</param>
|
|
/// <returns>Byte array representing the range, null on error</returns>
|
|
[Obsolete("Passthrough method that should not be used")]
|
|
public byte[]? ReadArbitraryRange(int rangeStart = -1, long length = -1)
|
|
{
|
|
// If we have an unset range start, read from the start of the source
|
|
if (rangeStart == -1)
|
|
rangeStart = 0;
|
|
|
|
// If we have an unset length, read the whole source
|
|
if (length == -1)
|
|
length = Length;
|
|
|
|
return ReadRangeFromSource(rangeStart, (int)length);
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|