Handle some big TODOs

This commit is contained in:
Matt Nadareski
2025-09-01 18:38:43 -04:00
parent 556e1c972c
commit 8eb5898ef6
31 changed files with 664 additions and 770 deletions

View File

@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Text;
using SabreTools.IO.Encryption;
using SabreTools.IO.Extensions;
using SabreTools.Models.MoPaQ;
using static SabreTools.Models.MoPaQ.Constants;

View File

@@ -10,7 +10,6 @@ namespace SabreTools.Serialization.Deserializers
public class NewExecutable : BaseBinaryDeserializer<Executable>
{
/// <inheritdoc/>
/// TODO: Relocation data needs to be hooked up when Models updated
public override Executable? Deserialize(Stream? data)
{
// If the data is invalid
@@ -64,7 +63,7 @@ namespace SabreTools.Serialization.Deserializers
executable.SegmentTable = new SegmentTableEntry[header.FileSegmentCount];
for (int i = 0; i < header.FileSegmentCount; i++)
{
executable.SegmentTable[i] = ParseSegmentTableEntry(data);
executable.SegmentTable[i] = ParseSegmentTableEntry(data, initialOffset);
}
#endregion
@@ -624,8 +623,9 @@ namespace SabreTools.Serialization.Deserializers
/// 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)
public static SegmentTableEntry ParseSegmentTableEntry(Stream data, long initialOffset)
{
var obj = new SegmentTableEntry();
@@ -634,6 +634,30 @@ namespace SabreTools.Serialization.Deserializers
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.Seek(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.Seek(currentOffset, SeekOrigin.Begin);
return obj;
}
}

View File

@@ -97,8 +97,7 @@ namespace SabreTools.Serialization.Deserializers
blocks[i] = block;
}
// TODO: Make this a direct assignment when Models is updated
obj.Blocks = [.. blocks];
obj.Blocks = blocks;
#endregion

View File

@@ -79,15 +79,6 @@ namespace SabreTools.Serialization.Deserializers
header.Version = data.ReadBytes(versionOffset);
wisOffset = offset;
}
bool earlyReturn = false;
// If the header is invalid
if (header.Version == null)
earlyReturn = true;
if (wisOffset < 0)
earlyReturn = true;
if (headerLength < 0)
earlyReturn = true;
//Seek back to the beginning of the section
data.Seek(initialOffset, 0);
@@ -101,10 +92,13 @@ namespace SabreTools.Serialization.Deserializers
header.FirstExecutableFileEntryLength = data.ReadUInt32LittleEndian();
header.MsiFileEntryLength = data.ReadUInt32LittleEndian();
if (earlyReturn)
{
// If the reported header information is invalid
if (header.Version == null)
return header;
if (wisOffset < 0)
return header;
if (headerLength < 0)
return header;
}
if (headerLength > 6)
{
@@ -155,7 +149,6 @@ namespace SabreTools.Serialization.Deserializers
header.Strings = stringArrays;
// Not sure what this data is. Might be a wisescript?
// TODO: Should really be done in the wrapper, but almost everything there is static so there's no good place\
if (header.UnknownDataSize != 0)
data.Seek(header.UnknownDataSize, SeekOrigin.Current);
@@ -216,9 +209,8 @@ namespace SabreTools.Serialization.Deserializers
/// <summary>
/// Parse the string table, if possible
/// </summary>
/// <param name="data"></param>
/// <param name="header"></param>
/// <param name="preStringBytesSize"></param>
/// <param name="data">Stream to parse</param>
/// <param name="preStringValues">Pre-string byte array containing string lengths</param>
/// <returns>The filled string table on success, false otherwise</returns>
private static byte[][]? ParseStringTable(Stream data, byte[] preStringValues)
{

View File

@@ -1186,51 +1186,9 @@ namespace SabreTools.Serialization
while (offset < entry.Data.Length && (offset % 4) != 0)
versionInfo.Padding2 = entry.Data.ReadUInt16LittleEndian(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.ReadNullTerminatedUnicodeString(ref offset);
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.ReadNullTerminatedUnicodeString(ref offset);
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;
}
}
// Determine if we have a StringFileInfo or VarFileInfo twice
ReadInfoSection(entry.Data, ref offset, versionInfo);
ReadInfoSection(entry.Data, ref offset, versionInfo);
return versionInfo;
}
@@ -1408,6 +1366,38 @@ namespace SabreTools.Serialization
return obj;
}
/// <summary>
/// Read either a `StringFileInfo` or `VarFileInfo` based on the key
/// </summary>
/// <param name="entry"></param>
/// <param name="offset"></param>
/// <param name="versionInfo"></param>
/// <returns></returns>
private static void ReadInfoSection(byte[] data, ref int offset, VersionInfo versionInfo)
{
// If the offset is invalid, don't move the pointer
if (offset < 0 || offset >= versionInfo.Length)
return;
// Cache the current offset for reading
int currentOffset = offset;
offset += 6;
string? nextKey = data.ReadNullTerminatedUnicodeString(ref offset);
offset = currentOffset;
if (nextKey == "StringFileInfo")
{
var stringFileInfo = AsStringFileInfo(data, ref offset);
versionInfo.StringFileInfo = stringFileInfo;
}
else if (nextKey == "VarFileInfo")
{
var varFileInfo = AsVarFileInfo(data, ref offset);
versionInfo.VarFileInfo = varFileInfo;
}
}
#endregion
#region Helpers

View File

@@ -1,173 +0,0 @@
using System;
using System.IO;
using SabreTools.Hashing;
using SabreTools.Matching;
using static SabreTools.Models.MoPaQ.Constants;
namespace SabreTools.Serialization
{
/// <summary>
/// Handler for decrypting MoPaQ block and table data
/// </summary>
/// TODO: Should this live in IO? New `Encryption` namespace?
public class MoPaQDecrypter
{
#region Private Instance Variables
/// <summary>
/// Buffer for encryption and decryption
/// </summary>
private readonly uint[] _stormBuffer = new uint[STORM_BUFFER_SIZE];
#endregion
public MoPaQDecrypter()
{
PrepareCryptTable();
}
/// <summary>
/// Prepare the encryption table
/// </summary>
private void PrepareCryptTable()
{
uint seed = 0x00100001;
for (uint index1 = 0; index1 < 0x100; index1++)
{
for (uint index2 = index1, i = 0; i < 5; i++, index2 += 0x100)
{
seed = (seed * 125 + 3) % 0x2AAAAB;
uint temp1 = (seed & 0xFFFF) << 0x10;
seed = (seed * 125 + 3) % 0x2AAAAB;
uint temp2 = (seed & 0xFFFF);
_stormBuffer[index2] = (temp1 | temp2);
}
}
}
/// <summary>
/// Load a table block by optionally decompressing and
/// decrypting before returning the data.
/// </summary>
/// <param name="data">Stream to parse</param>
/// <param name="offset">Data offset to parse</param>
/// <param name="expectedHash">Optional MD5 hash for validation</param>
/// <param name="compressedSize">Size of the table in the file</param>
/// <param name="tableSize">Expected size of the table</param>
/// <param name="key">Encryption key to use</param>
/// <param name="realTableSize">Output represening the real table size</param>
/// <returns>Byte array representing the processed table</returns>
public byte[]? LoadTable(Stream data,
long offset,
byte[]? expectedHash,
uint compressedSize,
uint tableSize,
uint key,
out long realTableSize)
{
byte[]? tableData;
byte[]? readBytes;
long bytesToRead = tableSize;
// Allocate the MPQ table
tableData = readBytes = new byte[tableSize];
// Check if the MPQ table is compressed
if (compressedSize != 0 && compressedSize < tableSize)
{
// Allocate temporary buffer for holding compressed data
readBytes = new byte[compressedSize];
bytesToRead = compressedSize;
}
// Get the file offset from which we will read the table
// Note: According to Storm.dll from Warcraft III (version 2002),
// if the hash table position is 0xFFFFFFFF, no SetFilePointer call is done
// and the table is loaded from the current file offset
if (offset == 0xFFFFFFFF)
offset = data.Position;
// Is the sector table within the file?
if (offset >= data.Length)
{
realTableSize = 0;
return null;
}
// The hash table and block table can go beyond EOF.
// Storm.dll reads as much as possible, then fills the missing part with zeros.
// Abused by Spazzler map protector which sets hash table size to 0x00100000
// Abused by NP_Protect in MPQs v4 as well
if ((offset + bytesToRead) > data.Length)
bytesToRead = (uint)(data.Length - offset);
// Give the caller information that the table was cut
realTableSize = bytesToRead;
// If everything succeeded, read the raw table from the MPQ
data.Seek(offset, SeekOrigin.Begin);
_ = data.Read(readBytes, 0, (int)bytesToRead);
// Verify the MD5 of the table, if present
byte[]? actualHash = HashTool.GetByteArrayHashArray(readBytes, HashType.MD5);
if (expectedHash != null && actualHash != null && !actualHash.EqualsExactly(expectedHash))
{
Console.WriteLine("Table is corrupt!");
return null;
}
// First of all, decrypt the table
if (key != 0)
tableData = DecryptBlock(readBytes, bytesToRead, key);
// If the table is compressed, decompress it
if (compressedSize != 0 && compressedSize < tableSize)
{
Console.WriteLine("Table is compressed, it will not read properly!");
return null;
// TODO: Handle decompression
// int cbOutBuffer = (int)tableSize;
// int cbInBuffer = (int)compressedSize;
// if (!SCompDecompress2(readBytes, &cbOutBuffer, tableData, cbInBuffer))
// errorCode = SErrGetLastError();
// tableData = readBytes;
}
// Return the MPQ table
return tableData;
}
/// <summary>
/// Decrypt a single block of data
/// </summary>
public unsafe byte[] DecryptBlock(byte[] block, long length, uint key)
{
uint seed = 0xEEEEEEEE;
uint[] castBlock = new uint[length >> 2];
Buffer.BlockCopy(block, 0, castBlock, 0, (int)length);
int castBlockPtr = 0;
// Round to uints
length >>= 2;
while (length-- > 0)
{
seed += _stormBuffer[MPQ_HASH_KEY2_MIX + (key & 0xFF)];
uint ch = castBlock[castBlockPtr] ^ (key + seed);
key = ((~key << 0x15) + 0x11111111) | (key >> 0x0B);
seed = ch + seed + (seed << 5) + 3;
castBlock[castBlockPtr++] = ch;
}
Buffer.BlockCopy(castBlock, 0, block, 0, block.Length >> 2);
return block;
}
}
}

View File

@@ -1,5 +1,6 @@
using System.Text;
using SabreTools.Models.BSP;
using SabreTools.Models.TAR;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Printers
@@ -144,10 +145,21 @@ namespace SabreTools.Serialization.Printers
for (int i = 0; i < lump.Entities.Length; i++)
{
// TODO: Implement entity printing
var entity = lump.Entities[i];
builder.AppendLine($" Entity {i}: Not printed yet");
builder.AppendLine($" Entity {i}:");
if (entity.Attributes == null || entity.Attributes.Count == 0)
{
builder.AppendLine(" No attributes");
continue;
}
for (int j = 0; j < entity.Attributes.Count; j++)
{
var kvp = entity.Attributes[j];
builder.AppendLine($" Attribute {j}: {kvp.Key}={kvp.Value}");
}
}
}

View File

@@ -17,7 +17,6 @@ namespace SabreTools.Serialization.Printers
builder.AppendLine();
Print(builder, file.Header);
// TODO: Capture or print the compressed data
Print(builder, file.Trailer);
}

View File

@@ -1,6 +1,7 @@
using System;
using System.IO;
using SabreTools.IO.Compression.Deflate;
using SabreTools.IO.Extensions;
using SabreTools.Models.BFPK;
using SabreTools.Serialization.Interfaces;
@@ -166,7 +167,7 @@ namespace SabreTools.Serialization.Wrappers
using FileStream fs = File.OpenWrite(filename);
// Read the data block
var data = ReadFromDataSource(offset, compressedSize);
var data = _dataSource.ReadFrom(offset, compressedSize, retainPosition: true);
if (data == null)
return false;

View File

@@ -1,5 +1,6 @@
using System;
using System.IO;
using SabreTools.IO.Extensions;
using SabreTools.Models.BSP;
using SabreTools.Serialization.Interfaces;
@@ -127,7 +128,7 @@ namespace SabreTools.Serialization.Wrappers
// Read the data
var lump = Lumps[index];
var data = ReadFromDataSource(lump.Offset, lump.Length);
var data = _dataSource.ReadFrom(lump.Offset, lump.Length, retainPosition: true);
if (data == null)
return false;

View File

@@ -322,7 +322,7 @@ namespace SabreTools.Serialization.Wrappers
return null;
// Try to read the sector data
var sectorData = ReadFromDataSource(sectorDataOffset, (int)SectorSize);
var sectorData = _dataSource.ReadFrom(sectorDataOffset, (int)SectorSize, retainPosition: true);
if (sectorData == null)
return null;

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using SabreTools.IO.Extensions;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
@@ -320,7 +321,7 @@ namespace SabreTools.Serialization.Wrappers
for (int i = 0; i < dataBlockOffsets.Count; i++)
{
int readSize = (int)Math.Min(BlockSize, fileSize);
var data = ReadFromDataSource((int)dataBlockOffsets[i], readSize);
var data = _dataSource.ReadFrom((int)dataBlockOffsets[i], readSize, retainPosition: true);
if (data == null)
return false;

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using SabreTools.IO.Compression.Blast;
using SabreTools.IO.Extensions;
using SabreTools.Models.InstallShieldArchiveV3;
using SabreTools.Serialization.Interfaces;
@@ -239,7 +240,7 @@ namespace SabreTools.Serialization.Wrappers
long outputFileSize = file.UncompressedSize;
// Read the compressed data directly
var compressedData = ReadFromDataSource((int)fileOffset, (int)fileSize);
var compressedData = _dataSource.ReadFrom((int)fileOffset, (int)fileSize, retainPosition: true);
if (compressedData == null)
return false;

View File

@@ -1,6 +1,7 @@
using System;
using System.IO;
using SabreTools.IO.Compression.SZDD;
using SabreTools.IO.Extensions;
using SabreTools.Models.LZ;
using SabreTools.Serialization.Interfaces;
@@ -110,7 +111,7 @@ namespace SabreTools.Serialization.Wrappers
return false;
// Read in the data as an array
byte[]? contents = ReadFromDataSource(DataOffset, (int)compressedSize);
byte[]? contents = _dataSource.ReadFrom(DataOffset, (int)compressedSize, retainPosition: true);
if (contents == null)
return false;

View File

@@ -1,6 +1,7 @@
using System;
using System.IO;
using SabreTools.IO.Compression.SZDD;
using SabreTools.IO.Extensions;
using SabreTools.Models.LZ;
using SabreTools.Serialization.Interfaces;
@@ -94,7 +95,7 @@ namespace SabreTools.Serialization.Wrappers
return false;
// Read in the data as an array
byte[]? contents = ReadFromDataSource(12, (int)compressedSize);
byte[]? contents = _dataSource.ReadFrom(12, (int)compressedSize, retainPosition: true);
if (contents == null)
return false;

View File

@@ -1,6 +1,7 @@
using System;
using System.IO;
using SabreTools.IO.Compression.SZDD;
using SabreTools.IO.Extensions;
using SabreTools.Models.LZ;
using SabreTools.Serialization.Interfaces;
@@ -110,7 +111,7 @@ namespace SabreTools.Serialization.Wrappers
return false;
// Read in the data as an array
byte[]? contents = ReadFromDataSource(14, (int)compressedSize);
byte[]? contents = _dataSource.ReadFrom(14, (int)compressedSize, retainPosition: true);
if (contents == null)
return false;

View File

@@ -139,7 +139,7 @@ namespace SabreTools.Serialization.Wrappers
return [];
// Read the entry data and return
return ReadFromDataSource(offset, length);
return _dataSource.ReadFrom(offset, length, retainPosition: true);
}
/// <summary>
@@ -315,7 +315,7 @@ namespace SabreTools.Serialization.Wrappers
if (length == -1)
length = Length;
return ReadFromDataSource(rangeStart, (int)length);
return _dataSource.ReadFrom(rangeStart, (int)length, retainPosition: true);
}
#endregion

View File

@@ -176,7 +176,7 @@ namespace SabreTools.Serialization.Wrappers
// Otherwise, cache and return the data
long overlayLength = dataLength - endOfSectionData;
_overlayData = ReadFromDataSource((int)endOfSectionData, (int)overlayLength);
_overlayData = _dataSource.ReadFrom((int)endOfSectionData, (int)overlayLength, retainPosition: true);
return _overlayData;
}
}
@@ -248,7 +248,7 @@ namespace SabreTools.Serialization.Wrappers
long overlayLength = Math.Min(dataLength - endOfSectionData, 16 * 1024 * 1024);
// Otherwise, cache and return the strings
_overlayStrings = ReadStringsFromDataSource(endOfSectionData, (int)overlayLength, charLimit: 3);
_overlayStrings = _dataSource.ReadStringsFrom(endOfSectionData, (int)overlayLength, charLimit: 3);
return _overlayStrings;
}
}
@@ -285,7 +285,7 @@ namespace SabreTools.Serialization.Wrappers
// Populate the raw stub executable data based on the source
int endOfStubHeader = 0x40;
int lengthOfStubExecutableData = (int)Stub.Header.NewExeHeaderAddr - endOfStubHeader;
_stubExecutableData = ReadFromDataSource(endOfStubHeader, lengthOfStubExecutableData);
_stubExecutableData = _dataSource.ReadFrom(endOfStubHeader, lengthOfStubExecutableData, retainPosition: true);
// Cache and return the stub executable data, even if null
return _stubExecutableData;
@@ -398,13 +398,14 @@ namespace SabreTools.Serialization.Wrappers
/// <remarks>
/// This extracts the following data:
/// - Archives and executables in the overlay
/// - Wise installers
/// </remarks>
public bool Extract(string outputDirectory, bool includeDebug)
{
bool overlay = ExtractFromOverlay(outputDirectory, includeDebug);
// TODO: Add Wise installer handling here
bool wise = ExtractWise(outputDirectory, includeDebug);
return overlay;
return overlay | wise;
}
/// <summary>
@@ -512,6 +513,71 @@ namespace SabreTools.Serialization.Wrappers
}
}
/// <summary>
/// Extract data from a Wise installer
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
public bool ExtractWise(string outputDirectory, bool includeDebug)
{
// Get the source data for reading
Stream source = _dataSource;
if (Filename != null)
{
// Try to open a multipart file
if (WiseOverlayHeader.OpenFile(Filename, includeDebug, out var temp) && temp != null)
source = temp;
}
// Try to find the overlay header
long offset = FindWiseOverlayHeader(includeDebug);
if (offset < 0)
{
if (includeDebug) Console.Error.WriteLine("Could not find the overlay header");
return false;
}
// Seek to the overlay and parse
source.Seek(offset, SeekOrigin.Begin);
var header = WiseOverlayHeader.Create(source);
if (header == null)
{
if (includeDebug) Console.Error.WriteLine("Could not parse the overlay header");
return false;
}
// Extract the header-defined files
bool extracted = header.ExtractHeaderDefinedFiles(outputDirectory, includeDebug, out long dataStart);
if (!extracted)
{
if (includeDebug) Console.Error.WriteLine("Could not extract header-defined files");
return false;
}
// Open the script file from the output directory
var scriptStream = File.OpenRead(Path.Combine(outputDirectory, "WiseScript.bin"));
var script = WiseScript.Create(scriptStream);
if (script == null)
{
if (includeDebug) Console.Error.WriteLine("Could not parse WiseScript.bin");
return false;
}
// Get the source directory
string? sourceDirectory = null;
if (Filename != null)
sourceDirectory = Path.GetDirectoryName(Path.GetFullPath(Filename));
// Process the state machine
return script.ProcessStateMachine(_dataSource,
sourceDirectory,
dataStart,
outputDirectory,
header.IsPKZIP,
includeDebug);
}
#endregion
#region Resources
@@ -579,7 +645,7 @@ namespace SabreTools.Serialization.Wrappers
return [];
// Read the resource data and return
return ReadFromDataSource(offset, length);
return _dataSource.ReadFrom(offset, length, retainPosition: true);
}
/// <summary>
@@ -628,6 +694,45 @@ namespace SabreTools.Serialization.Wrappers
return offset;
}
/// <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(bool includeDebug)
{
// Get the overlay offset
long overlayOffset = OverlayAddress;
if (overlayOffset < 0 || overlayOffset >= Length)
{
if (includeDebug) Console.Error.WriteLine("Could not parse the overlay header");
return -1;
}
// Attempt to get the overlay header
_dataSource.Seek(overlayOffset, SeekOrigin.Begin);
var header = Create(_dataSource);
if (header != null)
return overlayOffset;
// Align and loop to see if it can be found
_dataSource.Seek(overlayOffset, SeekOrigin.Begin);
_dataSource.AlignToBoundary(0x10);
overlayOffset = _dataSource.Position;
while (_dataSource.Position < Length)
{
_dataSource.Seek(overlayOffset, SeekOrigin.Begin);
header = Create(_dataSource);
if (header != null)
return overlayOffset;
overlayOffset += 0x10;
}
header = null;
return -1;
}
#endregion
#region Segments
@@ -671,7 +776,7 @@ namespace SabreTools.Serialization.Wrappers
return [];
// Read the segment data and return
return ReadFromDataSource(offset, length);
return _dataSource.ReadFrom(offset, length, retainPosition: true);
}
/// <summary>
@@ -741,7 +846,7 @@ namespace SabreTools.Serialization.Wrappers
if (length == -1)
length = Length;
return ReadFromDataSource(rangeStart, (int)length);
return _dataSource.ReadFrom(rangeStart, (int)length, retainPosition: true);
}
#endregion

View File

@@ -1,5 +1,6 @@
using System;
using System.IO;
using SabreTools.IO.Extensions;
using SabreTools.Models.PAK;
using SabreTools.Serialization.Interfaces;
@@ -127,7 +128,7 @@ namespace SabreTools.Serialization.Wrappers
// Read the item data
var directoryItem = DirectoryItems[index];
var data = ReadFromDataSource((int)directoryItem.ItemOffset, (int)directoryItem.ItemLength);
var data = _dataSource.ReadFrom((int)directoryItem.ItemOffset, (int)directoryItem.ItemLength, retainPosition: true);
if (data == null)
return false;

View File

@@ -1,5 +1,6 @@
using System;
using System.IO;
using SabreTools.IO.Extensions;
using SabreTools.Models.PFF;
using SabreTools.Serialization.Interfaces;
@@ -158,7 +159,7 @@ namespace SabreTools.Serialization.Wrappers
using FileStream fs = File.OpenWrite(filename);
// Read the data block
var data = ReadFromDataSource(offset, size);
var data = _dataSource.ReadFrom(offset, size, retainPosition: true);
if (data == null)
return false;

View File

@@ -84,7 +84,7 @@ namespace SabreTools.Serialization.Wrappers
return _entryPointData;
// Read the first 128 bytes of the entry point
_entryPointData = ReadFromDataSource(entryPointAddress, length: 128);
_entryPointData = _dataSource.ReadFrom(entryPointAddress, length: 128, retainPosition: true);
// Cache and return the entry point padding data, even if null
return _entryPointData;
@@ -135,7 +135,7 @@ namespace SabreTools.Serialization.Wrappers
if (headerLength <= 0)
_headerPaddingData = [];
else
_headerPaddingData = ReadFromDataSource((int)headerStartAddress, headerLength);
_headerPaddingData = _dataSource.ReadFrom((int)headerStartAddress, headerLength, retainPosition: true);
// Cache and return the header padding data, even if null
return _headerPaddingData;
@@ -183,7 +183,7 @@ namespace SabreTools.Serialization.Wrappers
if (headerLength <= 0)
_headerPaddingStrings = [];
else
_headerPaddingStrings = ReadStringsFromDataSource((int)headerStartAddress, headerLength, charLimit: 3);
_headerPaddingStrings = _dataSource.ReadStringsFrom((int)headerStartAddress, headerLength, charLimit: 3);
// Cache and return the header padding data, even if null
return _headerPaddingStrings;
@@ -333,7 +333,7 @@ namespace SabreTools.Serialization.Wrappers
// Otherwise, cache and return the data
long overlayLength = dataLength - endOfSectionData;
_overlayData = ReadFromDataSource(endOfSectionData, (int)overlayLength);
_overlayData = _dataSource.ReadFrom(endOfSectionData, (int)overlayLength, retainPosition: true);
return _overlayData;
}
}
@@ -414,7 +414,7 @@ namespace SabreTools.Serialization.Wrappers
long overlayLength = Math.Min(dataLength - endOfSectionData, 16 * 1024 * 1024);
// Otherwise, cache and return the strings
_overlayStrings = ReadStringsFromDataSource(endOfSectionData, (int)overlayLength, charLimit: 3);
_overlayStrings = _dataSource.ReadStringsFrom(endOfSectionData, (int)overlayLength, charLimit: 3);
return _overlayStrings;
}
}
@@ -487,7 +487,7 @@ namespace SabreTools.Serialization.Wrappers
// Populate the raw stub executable data based on the source
int endOfStubHeader = 0x40;
int lengthOfStubExecutableData = (int)Stub.Header.NewExeHeaderAddr - endOfStubHeader;
_stubExecutableData = ReadFromDataSource(endOfStubHeader, lengthOfStubExecutableData);
_stubExecutableData = _dataSource.ReadFrom(endOfStubHeader, lengthOfStubExecutableData, retainPosition: true);
// Cache and return the stub executable data, even if null
return _stubExecutableData;
@@ -1029,7 +1029,7 @@ namespace SabreTools.Serialization.Wrappers
byte[]? entryData;
try
{
entryData = ReadFromDataSource((int)address, (int)size);
entryData = _dataSource.ReadFrom((int)address, (int)size, retainPosition: true);
if (entryData == null || entryData.Length < 4)
continue;
}
@@ -1088,15 +1088,16 @@ namespace SabreTools.Serialization.Wrappers
/// - Archives and executables in resource data
/// - CExe-compressed resource data
/// - SFX archives (7z, PKZIP, RAR)
/// - Wise installers
/// </remarks>
public bool Extract(string outputDirectory, bool includeDebug)
{
bool cexe = ExtractCExe(outputDirectory, includeDebug);
bool overlay = ExtractFromOverlay(outputDirectory, includeDebug);
bool resources = ExtractFromResources(outputDirectory, includeDebug);
// TODO: Add Wise installer handling here
bool wise = ExtractWise(outputDirectory, includeDebug);
return cexe || overlay || resources;
return cexe || overlay || resources | wise;
}
/// <summary>
@@ -1366,6 +1367,37 @@ namespace SabreTools.Serialization.Wrappers
}
}
/// <summary>
/// Extract data from a Wise installer
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
public bool ExtractWise(string outputDirectory, bool includeDebug)
{
// Get the source data for reading
Stream source = _dataSource;
if (Filename != null)
{
// Try to open a multipart file
if (WiseOverlayHeader.OpenFile(Filename, includeDebug, out var temp) && temp != null)
source = temp;
}
// Try to find the overlay header
long offset = FindWiseOverlayHeader(includeDebug);
if (offset > 0 && offset < Length)
return ExtractWiseOverlay(outputDirectory, includeDebug, source, offset);
// Try to find the section header
offset = FindWiseSectionHeader(includeDebug);
if (offset > 0 && offset < Length)
return ExtractWiseSection(outputDirectory, includeDebug, source, offset);
// Everything else could not extract
return false;
}
/// <summary>
/// Decompress CExe data compressed with LZ
/// </summary>
@@ -1435,6 +1467,81 @@ namespace SabreTools.Serialization.Wrappers
}
}
/// <summary>
/// Extract using Wise overlay
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <param name="source">Potentially multi-part stream to read</param>
/// <param name="offset">Offset to the start of the overlay header</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
private bool ExtractWiseOverlay(string outputDirectory, bool includeDebug, Stream source, long offset)
{
// Seek to the overlay and parse
source.Seek(offset, SeekOrigin.Begin);
var header = WiseOverlayHeader.Create(source);
if (header == null)
{
if (includeDebug) Console.Error.WriteLine("Could not parse the overlay header");
return false;
}
// Extract the header-defined files
bool extracted = header.ExtractHeaderDefinedFiles(outputDirectory, includeDebug, out long dataStart);
if (!extracted)
{
if (includeDebug) Console.Error.WriteLine("Could not extract header-defined files");
return false;
}
// Open the script file from the output directory
var scriptStream = File.OpenRead(Path.Combine(outputDirectory, "WiseScript.bin"));
var script = WiseScript.Create(scriptStream);
if (script == null)
{
if (includeDebug) Console.Error.WriteLine("Could not parse WiseScript.bin");
return false;
}
// Get the source directory
string? sourceDirectory = null;
if (Filename != null)
sourceDirectory = Path.GetDirectoryName(Path.GetFullPath(Filename));
// Process the state machine
return script.ProcessStateMachine(_dataSource,
sourceDirectory,
dataStart,
outputDirectory,
header.IsPKZIP,
includeDebug);
}
/// <summary>
/// Extract using Wise section
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <param name="source">Potentially multi-part stream to read</param>
/// <param name="offset">Offset to the start of the section header</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
private bool ExtractWiseSection(string outputDirectory, bool includeDebug, Stream source, long offset)
{
// Get the size of the section and seek to the start
source.Seek(offset, SeekOrigin.Begin);
// Write section data to new stream
var header = WiseSectionHeader.Create(source);
if (header == null)
{
if (includeDebug) Console.Error.WriteLine("Could not parse the section header");
return false;
}
// Attempt to extract section
return header.Extract(outputDirectory, includeDebug);
}
#endregion
#region Resource Data
@@ -1620,6 +1727,149 @@ namespace SabreTools.Serialization.Wrappers
return 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(bool includeDebug)
{
// Get the overlay offset
long overlayOffset = OverlayAddress;
// Attempt to get the overlay header
if (overlayOffset >= 0 && overlayOffset < Length)
{
_dataSource.Seek(overlayOffset, SeekOrigin.Begin);
var header = Create(_dataSource);
if (header != null)
return overlayOffset;
}
// Check section data
foreach (var section in SectionTable ?? [])
{
string sectionName = Encoding.ASCII.GetString(section.Name ?? []).TrimEnd('\0');
long sectionOffset = section.VirtualAddress.ConvertVirtualAddress(SectionTable);
_dataSource.Seek(sectionOffset, SeekOrigin.Begin);
var header = Create(_dataSource);
if (header != null)
return sectionOffset;
// Check after the resource table
if (sectionName == ".rsrc")
{
// Data immediately following
long afterResourceOffset = sectionOffset + section.SizeOfRawData;
_dataSource.Seek(afterResourceOffset, SeekOrigin.Begin);
header = Create(_dataSource);
if (header != null)
return afterResourceOffset;
// Data following padding data
_dataSource.Seek(afterResourceOffset, SeekOrigin.Begin);
_ = _dataSource.ReadNullTerminatedAnsiString();
afterResourceOffset = _dataSource.Position;
header = Create(_dataSource);
if (header != null)
return afterResourceOffset;
}
}
// If there are no resources
if (OptionalHeader?.ResourceTable == null || ResourceData == null)
return -1;
// Get the resources that have an executable signature
bool exeResources = false;
foreach (var kvp in ResourceData)
{
if (kvp.Value == null || kvp.Value is not byte[] ba)
continue;
if (!ba.StartsWith(Models.MSDOS.Constants.SignatureBytes))
continue;
exeResources = true;
break;
}
// If there are no executable resources
if (!exeResources)
{
if (includeDebug) Console.Error.WriteLine("Could not find the overlay header");
return -1;
}
// Get the raw resource table offset
long resourceTableOffset = OptionalHeader.ResourceTable.VirtualAddress.ConvertVirtualAddress(SectionTable);
if (resourceTableOffset <= 0)
{
if (includeDebug) Console.Error.WriteLine("Could not find the overlay header");
return -1;
}
// Search the resource table data for the offset
long resourceOffset = -1;
_dataSource.Seek(resourceTableOffset, SeekOrigin.Begin);
while (_dataSource.Position < resourceTableOffset + OptionalHeader.ResourceTable.Size && _dataSource.Position < _dataSource.Length)
{
ushort possibleSignature = _dataSource.ReadUInt16();
if (possibleSignature == Models.MSDOS.Constants.SignatureUInt16)
{
resourceOffset = _dataSource.Position - 2;
break;
}
_dataSource.Seek(-1, SeekOrigin.Current);
}
// If there was no valid offset, somehow
if (resourceOffset == -1)
{
if (includeDebug) Console.Error.WriteLine("Could not find the overlay header");
return -1;
}
// Parse the executable and recurse
_dataSource.Seek(resourceOffset, SeekOrigin.Begin);
var resourceExe = WrapperFactory.CreateExecutableWrapper(_dataSource);
if (resourceExe is not PortableExecutable resourcePex)
{
if (includeDebug) Console.Error.WriteLine("Could not find the overlay header");
return -1;
}
return resourcePex.FindWiseOverlayHeader(includeDebug);
}
/// <summary>
/// Find the location of a Wise section header, if it exists
/// </summary>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>Offset to the section header on success, -1 otherwise</returns>
public long FindWiseSectionHeader(bool includeDebug)
{
// If the section table is invalid
if (SectionTable == null)
return -1;
// Find the .WISE section
foreach (var section in SectionTable)
{
string sectionName = Encoding.ASCII.GetString(section.Name ?? []).TrimEnd('\0');
if (sectionName != ".WISE")
continue;
return section.VirtualAddress.ConvertVirtualAddress(SectionTable);
}
// Otherwise, it could not be found
return -1;
}
#endregion
#region Resource Parsing
@@ -1966,7 +2216,7 @@ namespace SabreTools.Serialization.Wrappers
return _sectionData[index];
// Populate the raw section data based on the source
byte[]? sectionData = ReadFromDataSource((int)address, (int)size);
byte[]? sectionData = _dataSource.ReadFrom((int)address, (int)size, retainPosition: true);
// Cache and return the section data, even if null
_sectionData[index] = sectionData ?? [];
@@ -2052,7 +2302,7 @@ namespace SabreTools.Serialization.Wrappers
return _sectionStringData[index];
// Populate the section string data based on the source
List<string>? sectionStringData = ReadStringsFromDataSource((int)address, (int)size);
List<string>? sectionStringData = _dataSource.ReadStringsFrom((int)address, (int)size);
// Cache and return the section string data, even if null
_sectionStringData[index] = sectionStringData ?? [];
@@ -2136,7 +2386,7 @@ namespace SabreTools.Serialization.Wrappers
return _tableData[index];
// Populate the raw table data based on the source
byte[]? tableData = ReadFromDataSource((int)address, (int)size);
byte[]? tableData = _dataSource.ReadFrom((int)address, (int)size, retainPosition: true);
// Cache and return the table data, even if null
_tableData[index] = tableData ?? [];
@@ -2181,7 +2431,7 @@ namespace SabreTools.Serialization.Wrappers
return _tableStringData[index];
// Populate the table string data based on the source
List<string>? tableStringData = ReadStringsFromDataSource((int)address, (int)size);
List<string>? tableStringData = _dataSource.ReadStringsFrom((int)address, (int)size);
// Cache and return the table string data, even if null
_tableStringData[index] = tableStringData ?? [];

View File

@@ -1,5 +1,6 @@
using System;
using System.IO;
using SabreTools.IO.Extensions;
using SabreTools.Models.Quantum;
using SabreTools.Serialization.Interfaces;
@@ -140,7 +141,7 @@ namespace SabreTools.Serialization.Wrappers
// Read the entire compressed data
int compressedDataOffset = (int)CompressedDataOffset;
long compressedDataLength = Length - compressedDataOffset;
var compressedData = ReadFromDataSource(compressedDataOffset, (int)compressedDataLength);
var compressedData = _dataSource.ReadFrom(compressedDataOffset, (int)compressedDataLength, retainPosition: true);
// Print a debug reminder
if (includeDebug) Console.WriteLine("Quantum archive extraction is unsupported");

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using SabreTools.IO.Compression.zlib;
using SabreTools.IO.Extensions;
using SabreTools.Models.SGA;
using SabreTools.Serialization.Interfaces;
@@ -203,7 +204,7 @@ namespace SabreTools.Serialization.Wrappers
long outputFileSize = GetUncompressedSize(index);
// Read the compressed data directly
var compressedData = ReadFromDataSource((int)fileOffset, (int)fileSize);
var compressedData = _dataSource.ReadFrom((int)fileOffset, (int)fileSize, retainPosition: true);
if (compressedData == null)
return false;

View File

@@ -17,8 +17,7 @@ namespace SabreTools.Serialization.Wrappers
#region Extension Properties
/// <inheritdoc cref="Archive.Entries"/>
/// TODO: Simplify when Models is updated
public Entry[]? Entries => Model.Entries != null ? [.. Model.Entries] : null;
public Entry[]? Entries => Model.Entries;
#endregion

View File

@@ -1,5 +1,6 @@
using System;
using System.IO;
using SabreTools.IO.Extensions;
using SabreTools.Models.BSP;
using SabreTools.Serialization.Interfaces;
@@ -127,7 +128,7 @@ namespace SabreTools.Serialization.Wrappers
// Read the data
var lump = Lumps[index];
var data = ReadFromDataSource(lump.Offset, lump.Length);
var data = _dataSource.ReadFrom(lump.Offset, lump.Length, retainPosition: true);
if (data == null)
return false;

View File

@@ -1,5 +1,6 @@
using System;
using System.IO;
using SabreTools.IO.Extensions;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
@@ -126,7 +127,7 @@ namespace SabreTools.Serialization.Wrappers
// Read the data -- TODO: Handle uncompressed lumps (see BSP.ExtractTexture)
var lump = DirEntries[index];
var data = ReadFromDataSource((int)lump.Offset, (int)lump.Length);
var data = _dataSource.ReadFrom((int)lump.Offset, (int)lump.Length, retainPosition: true);
if (data == null)
return false;

View File

@@ -1,10 +1,7 @@
using System;
using System.IO;
using System.Text;
using SabreTools.IO.Compression.Deflate;
using SabreTools.IO.Extensions;
using SabreTools.IO.Streams;
using SabreTools.Matching;
using SabreTools.Models.WiseInstaller;
namespace SabreTools.Serialization.Wrappers
@@ -20,6 +17,59 @@ namespace SabreTools.Serialization.Wrappers
#region Extension Properties
/// <summary>
/// Returns the offset relative to the start of the header
/// where the compressed data lives
/// </summary>
public long CompressedDataOffset
{
get
{
long offset = 0;
if (Model.DllNameLen > 0)
{
offset += Model.DllNameLen;
offset += 4; // DllSize
}
offset += 4; // Flags
offset += 12; // GraphicsData
offset += 4; // WiseScriptExitEventOffset
offset += 4; // WiseScriptCancelEventOffset
offset += 4; // WiseScriptInflatedSize
offset += 4; // WiseScriptDeflatedSize
offset += 4; // WiseDllDeflatedSize
offset += 4; // Ctl3d32DeflatedSize
offset += 4; // SomeData4DeflatedSize
offset += 4; // RegToolDeflatedSize
offset += 4; // ProgressDllDeflatedSize
offset += 4; // SomeData7DeflatedSize
offset += 4; // SomeData8DeflatedSize
offset += 4; // SomeData9DeflatedSize
offset += 4; // SomeData10DeflatedSize
offset += 4; // FinalFileDeflatedSize
offset += 4; // FinalFileInflatedSize
offset += 4; // EOF
if (DibDeflatedSize == 0 && Model.Endianness == 0)
return offset;
offset += 4; // DibDeflatedSize
offset += 4; // DibInflatedSize
if (Model.InstallScriptDeflatedSize != null)
offset += 4; // InstallScriptDeflatedSize
if (Model.CharacterSet != null)
offset += 4; // CharacterSet
offset += 2; // Endianness
offset += Model.InitTextLen;
return offset;
}
}
/// <inheritdoc cref="OverlayHeader.Ctl3d32DeflatedSize"/>
public uint Ctl3d32DeflatedSize => Model.Ctl3d32DeflatedSize;
@@ -155,280 +205,86 @@ namespace SabreTools.Serialization.Wrappers
#region Extraction
/// <summary>
/// Extract all files from a Wise installer to an output directory
/// Extract the predefined, static files defined in the header
/// </summary>
/// <param name="filename">Input filename to read from</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if all files extracted, false otherwise</returns>
public static bool ExtractAll(string? filename, string outputDirectory, bool includeDebug)
/// <returns>True if the files extracted successfully, false otherwise</returns>
public bool ExtractHeaderDefinedFiles(string outputDirectory, bool includeDebug, out long dataStart)
{
// If the filename is invalid
if (filename == null)
// Seek to the compressed data offset
_dataSource.Seek(CompressedDataOffset, SeekOrigin.Begin);
// Determine where the remaining compressed data starts
dataStart = _dataSource.Position;
// Extract WiseColors.dib, if it exists
var expected = new DeflateInfo { InputSize = DibDeflatedSize, OutputSize = DibInflatedSize, Crc32 = 0 };
if (InflateWrapper.ExtractFile(_dataSource, "WiseColors.dib", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// If the file could not be opened
if (!OpenFile(filename, includeDebug, out var stream))
// Extract WiseScript.bin
expected = new DeflateInfo { InputSize = WiseScriptDeflatedSize, OutputSize = WiseScriptInflatedSize, Crc32 = 0 };
if (InflateWrapper.ExtractFile(_dataSource, "WiseScript.bin", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Get the source directory
string? sourceDirectory = Path.GetDirectoryName(Path.GetFullPath(filename));
return ExtractAll(stream, sourceDirectory, outputDirectory, includeDebug);
}
/// <summary>
/// Extract all files from a Wise installer to an output directory
/// </summary>
/// <param name="data">Stream representing the Wise installer</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if all files extracted, false otherwise</returns>
public static bool ExtractAll(Stream? data, string outputDirectory, bool includeDebug)
=> ExtractAll(data, sourceDirectory: null, outputDirectory, includeDebug);
/// <summary>
/// Extract all files from a Wise installer to an output directory
/// </summary>
/// <param name="data">Stream representing the Wise installer</param>
/// <param name="sourceDirectory">Directory where installer files live, if possible</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if all files extracted, false otherwise</returns>
public static bool ExtractAll(Stream? data, string? sourceDirectory, string outputDirectory, bool includeDebug)
{
// If the data is invalid
if (data == null || !data.CanRead)
// Extract WISE0001.DLL, if it exists
expected = new DeflateInfo { InputSize = WiseDllDeflatedSize, OutputSize = -1, Crc32 = 0 };
if (InflateWrapper.ExtractFile(_dataSource, "WISE0001.DLL", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Attempt to get the overlay header
if (!FindOverlayHeader(data, includeDebug, out var header) || header == null)
{
if (includeDebug) Console.Error.WriteLine("Could not parse the overlay header");
return false;
}
// Extract the header-defined files
bool extracted = header.ExtractHeaderDefinedFiles(data, outputDirectory, includeDebug, out long dataStart);
if (!extracted)
{
if (includeDebug) Console.Error.WriteLine("Could not extract header-defined files");
return false;
}
// Open the script file from the output directory
var scriptStream = File.OpenRead(Path.Combine(outputDirectory, "WiseScript.bin"));
var script = WiseScript.Create(scriptStream);
if (script == null)
{
if (includeDebug) Console.Error.WriteLine("Could not parse WiseScript.bin");
return false;
}
// Process the state machine
return script.ProcessStateMachine(data,
sourceDirectory,
dataStart,
outputDirectory,
header.IsPKZIP,
includeDebug);
}
/// <summary>
/// Find the overlay header from the Wise installer, if possible
/// </summary>
/// <param name="data">Stream representing the Wise installer</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <param name="header">The found overlay header on success, null otherwise</param>
/// <returns>True if the header was found and valid, false otherwise</returns>
public static bool FindOverlayHeader(Stream data, bool includeDebug, out WiseOverlayHeader? header)
{
// Set the default header value
header = null;
// Attempt to deserialize the file as either NE or PE
var wrapper = WrapperFactory.CreateExecutableWrapper(data);
if (wrapper is NewExecutable ne)
{
return FindOverlayHeader(data, ne, includeDebug, out header);
}
else if (wrapper is PortableExecutable pe)
{
return FindOverlayHeader(data, pe, includeDebug, out header);
}
else
{
if (includeDebug) Console.Error.WriteLine("Only NE and PE executables are supported");
return false;
}
}
/// <summary>
/// Find the overlay header from a NE Wise installer, if possible
/// </summary>
/// <param name="data">Stream representing the Wise installer</param>
/// <param name="nex">Wrapper representing the NE</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <param name="header">The found overlay header on success, null otherwise</param>
/// <returns>True if the header was found and valid, false otherwise</returns>
public static bool FindOverlayHeader(Stream data, NewExecutable nex, bool includeDebug, out WiseOverlayHeader? header)
{
// Set the default header value
header = null;
// Get the overlay offset
long overlayOffset = nex.OverlayAddress;
if (overlayOffset < 0 || overlayOffset >= data.Length)
{
if (includeDebug) Console.Error.WriteLine("Could not parse the overlay header");
return false;
}
// Attempt to get the overlay header
data.Seek(overlayOffset, SeekOrigin.Begin);
header = Create(data);
if (header != null)
return true;
// Align and loop to see if it can be found
data.Seek(overlayOffset, SeekOrigin.Begin);
data.AlignToBoundary(0x10);
overlayOffset = data.Position;
while (data.Position < data.Length)
{
data.Seek(overlayOffset, SeekOrigin.Begin);
header = Create(data);
if (header != null)
return true;
overlayOffset += 0x10;
}
header = null;
return false;
}
/// <summary>
/// Find the overlay header from a PE Wise installer, if possible
/// </summary>
/// <param name="data">Stream representing the Wise installer</param>
/// <param name="pex">Wrapper representing the PE</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <param name="header">The found overlay header on success, null otherwise</param>
/// <returns>True if the header was found and valid, false otherwise</returns>
public static bool FindOverlayHeader(Stream data, PortableExecutable pex, bool includeDebug, out WiseOverlayHeader? header)
{
// Set the default header value
header = null;
// Get the overlay offset
long overlayOffset = pex.OverlayAddress;
// Attempt to get the overlay header
if (overlayOffset >= 0 && overlayOffset < pex.Length)
{
data.Seek(overlayOffset, SeekOrigin.Begin);
header = Create(data);
if (header != null)
return true;
}
// Check section data
foreach (var section in pex.Model.SectionTable ?? [])
{
string sectionName = Encoding.ASCII.GetString(section.Name ?? []).TrimEnd('\0');
long sectionOffset = section.VirtualAddress.ConvertVirtualAddress(pex.Model.SectionTable);
data.Seek(sectionOffset, SeekOrigin.Begin);
header = Create(data);
if (header != null)
return true;
// Check after the resource table
if (sectionName == ".rsrc")
{
// Data immediately following
long afterResourceOffset = sectionOffset + section.SizeOfRawData;
data.Seek(afterResourceOffset, SeekOrigin.Begin);
header = Create(data);
if (header != null)
return true;
// Data following padding data
data.Seek(afterResourceOffset, SeekOrigin.Begin);
_ = data.ReadNullTerminatedAnsiString();
header = Create(data);
if (header != null)
return true;
}
}
// If there are no resources
if (pex.Model.OptionalHeader?.ResourceTable == null || pex.ResourceData == null)
// Extract CTL3D32.DLL, if it exists
expected = new DeflateInfo { InputSize = Ctl3d32DeflatedSize, OutputSize = -1, Crc32 = 0 };
if (InflateWrapper.ExtractFile(_dataSource, "CTL3D32.DLL", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Get the resources that have an executable signature
bool exeResources = false;
foreach (var kvp in pex.ResourceData)
{
if (kvp.Value == null || kvp.Value is not byte[] ba)
continue;
if (!ba.StartsWith(Models.MSDOS.Constants.SignatureBytes))
continue;
exeResources = true;
break;
}
// If there are no executable resources
if (!exeResources)
{
if (includeDebug) Console.Error.WriteLine("Could not find the overlay header");
// Extract FILE0004, if it exists
expected = new DeflateInfo { InputSize = SomeData4DeflatedSize, OutputSize = -1, Crc32 = 0 };
if (InflateWrapper.ExtractFile(_dataSource, "FILE0004", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
}
// Get the raw resource table offset
long resourceTableOffset = pex.Model.OptionalHeader.ResourceTable.VirtualAddress.ConvertVirtualAddress(pex.Model.SectionTable);
if (resourceTableOffset <= 0)
{
if (includeDebug) Console.Error.WriteLine("Could not find the overlay header");
// Extract Ocxreg32.EXE, if it exists
expected = new DeflateInfo { InputSize = RegToolDeflatedSize, OutputSize = -1, Crc32 = 0 };
if (InflateWrapper.ExtractFile(_dataSource, "Ocxreg32.EXE", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
}
// Search the resource table data for the offset
long resourceOffset = -1;
data.Seek(resourceTableOffset, SeekOrigin.Begin);
while (data.Position < resourceTableOffset + pex.Model.OptionalHeader.ResourceTable.Size && data.Position < data.Length)
{
ushort possibleSignature = data.ReadUInt16();
if (possibleSignature == Models.MSDOS.Constants.SignatureUInt16)
{
resourceOffset = data.Position - 2;
break;
}
data.Seek(-1, SeekOrigin.Current);
}
// If there was no valid offset, somehow
if (resourceOffset == -1)
{
if (includeDebug) Console.Error.WriteLine("Could not find the overlay header");
// Extract PROGRESS.DLL, if it exists
expected = new DeflateInfo { InputSize = ProgressDllDeflatedSize, OutputSize = -1, Crc32 = 0 };
if (InflateWrapper.ExtractFile(_dataSource, "PROGRESS.DLL", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
}
// Parse the executable and recurse
data.Seek(resourceOffset, SeekOrigin.Begin);
var resourceExe = WrapperFactory.CreateExecutableWrapper(data);
if (resourceExe is not PortableExecutable resourcePex)
{
if (includeDebug) Console.Error.WriteLine("Could not find the overlay header");
// Extract FILE0007, if it exists
expected = new DeflateInfo { InputSize = SomeData7DeflatedSize, OutputSize = -1, Crc32 = 0 };
if (InflateWrapper.ExtractFile(_dataSource, "FILE0007", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
}
return FindOverlayHeader(data, resourcePex, includeDebug, out header);
// Extract FILE0008, if it exists
expected = new DeflateInfo { InputSize = SomeData8DeflatedSize, OutputSize = -1, Crc32 = 0 };
if (InflateWrapper.ExtractFile(_dataSource, "FILE0008", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Extract FILE0009, if it exists
expected = new DeflateInfo { InputSize = SomeData9DeflatedSize, OutputSize = -1, Crc32 = 0 };
if (InflateWrapper.ExtractFile(_dataSource, "FILE0009", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Extract FILE000A, if it exists
expected = new DeflateInfo { InputSize = SomeData10DeflatedSize, OutputSize = -1, Crc32 = 0 };
if (InflateWrapper.ExtractFile(_dataSource, "FILE000A", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Extract install script, if it exists
expected = new DeflateInfo { InputSize = InstallScriptDeflatedSize, OutputSize = -1, Crc32 = 0 };
if (InflateWrapper.ExtractFile(_dataSource, "INSTALL_SCRIPT", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Extract FILE000{n}.DAT, if it exists
expected = new DeflateInfo { InputSize = FinalFileDeflatedSize, OutputSize = FinalFileInflatedSize, Crc32 = 0 };
if (InflateWrapper.ExtractFile(_dataSource, IsPKZIP ? null : "FILE00XX.DAT", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
dataStart = _dataSource.Position;
return true;
}
/// <summary>
@@ -437,7 +293,7 @@ namespace SabreTools.Serialization.Wrappers
/// <param name="filename">Input filename or base name to read from</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file could be opened, false otherwise</returns>
private static bool OpenFile(string filename, bool includeDebug, out ReadOnlyCompositeStream? stream)
public static bool OpenFile(string filename, bool includeDebug, out ReadOnlyCompositeStream? stream)
{
// If the file exists as-is
if (File.Exists(filename))
@@ -535,89 +391,6 @@ namespace SabreTools.Serialization.Wrappers
return true;
}
/// <summary>
/// Extract the predefined, static files defined in the header
/// </summary>
/// <param name="data">Stream representing the Wise installer</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the files extracted successfully, false otherwise</returns>
private bool ExtractHeaderDefinedFiles(Stream data, string outputDirectory, bool includeDebug, out long dataStart)
{
// TODO: This needs to seek to the end of the header before extracting
// Determine where the remaining compressed data starts
dataStart = data.Position;
// Extract WiseColors.dib, if it exists
var expected = new DeflateInfo { InputSize = DibDeflatedSize, OutputSize = DibInflatedSize, Crc32 = 0 };
if (InflateWrapper.ExtractFile(data, "WiseColors.dib", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Extract WiseScript.bin
expected = new DeflateInfo { InputSize = WiseScriptDeflatedSize, OutputSize = WiseScriptInflatedSize, Crc32 = 0 };
if (InflateWrapper.ExtractFile(data, "WiseScript.bin", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Extract WISE0001.DLL, if it exists
expected = new DeflateInfo { InputSize = WiseDllDeflatedSize, OutputSize = -1, Crc32 = 0 };
if (InflateWrapper.ExtractFile(data, "WISE0001.DLL", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Extract CTL3D32.DLL, if it exists
expected = new DeflateInfo { InputSize = Ctl3d32DeflatedSize, OutputSize = -1, Crc32 = 0 };
if (InflateWrapper.ExtractFile(data, "CTL3D32.DLL", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Extract FILE0004, if it exists
expected = new DeflateInfo { InputSize = SomeData4DeflatedSize, OutputSize = -1, Crc32 = 0 };
if (InflateWrapper.ExtractFile(data, "FILE0004", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Extract Ocxreg32.EXE, if it exists
expected = new DeflateInfo { InputSize = RegToolDeflatedSize, OutputSize = -1, Crc32 = 0 };
if (InflateWrapper.ExtractFile(data, "Ocxreg32.EXE", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Extract PROGRESS.DLL, if it exists
expected = new DeflateInfo { InputSize = ProgressDllDeflatedSize, OutputSize = -1, Crc32 = 0 };
if (InflateWrapper.ExtractFile(data, "PROGRESS.DLL", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Extract FILE0007, if it exists
expected = new DeflateInfo { InputSize = SomeData7DeflatedSize, OutputSize = -1, Crc32 = 0 };
if (InflateWrapper.ExtractFile(data, "FILE0007", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Extract FILE0008, if it exists
expected = new DeflateInfo { InputSize = SomeData8DeflatedSize, OutputSize = -1, Crc32 = 0 };
if (InflateWrapper.ExtractFile(data, "FILE0008", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Extract FILE0009, if it exists
expected = new DeflateInfo { InputSize = SomeData9DeflatedSize, OutputSize = -1, Crc32 = 0 };
if (InflateWrapper.ExtractFile(data, "FILE0009", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Extract FILE000A, if it exists
expected = new DeflateInfo { InputSize = SomeData10DeflatedSize, OutputSize = -1, Crc32 = 0 };
if (InflateWrapper.ExtractFile(data, "FILE000A", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Extract install script, if it exists
expected = new DeflateInfo { InputSize = InstallScriptDeflatedSize, OutputSize = -1, Crc32 = 0 };
if (InflateWrapper.ExtractFile(data, "INSTALL_SCRIPT", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
// Extract FILE000{n}.DAT, if it exists
expected = new DeflateInfo { InputSize = FinalFileDeflatedSize, OutputSize = FinalFileInflatedSize, Crc32 = 0 };
if (InflateWrapper.ExtractFile(data, IsPKZIP ? null : "FILE00XX.DAT", outputDirectory, expected, IsPKZIP, includeDebug) == ExtractionStatus.FAIL)
return false;
dataStart = data.Position;
return true;
}
#endregion
}
}

View File

@@ -3,7 +3,9 @@ using System.Collections.Generic;
using System.IO;
using System.Text;
using SabreTools.IO.Compression.Deflate;
#if NETFRAMEWORK || NETSTANDARD
using SabreTools.IO.Extensions;
#endif
using SabreTools.Models.WiseInstaller;
using SabreTools.Models.WiseInstaller.Actions;
@@ -472,12 +474,10 @@ namespace SabreTools.Serialization.Wrappers
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
using (var iniFile = File.Open(iniFilePath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite))
{
iniFile.Write(Encoding.ASCII.GetBytes($"[{obj.Section}]\n"));
iniFile.Write(Encoding.ASCII.GetBytes($"{obj.Values ?? string.Empty}\n"));
iniFile.Flush();
}
using var iniFile = File.Open(iniFilePath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite);
iniFile.Write(Encoding.ASCII.GetBytes($"[{obj.Section}]\n"));
iniFile.Write(Encoding.ASCII.GetBytes($"{obj.Values ?? string.Empty}\n"));
iniFile.Flush();
}
#endregion

View File

@@ -19,6 +19,54 @@ namespace SabreTools.Serialization.Wrappers
#region Extension Properties
/// <summary>
/// Returns the offset relative to the start of the header
/// where the compressed data lives
/// </summary>
public long CompressedDataOffset
{
get
{
long offset = 0;
offset += 4; // UnknownDataSize
offset += 4; // SecondExecutableFileEntryLength
offset += 4; // UnknownValue2
offset += 4; // UnknownValue3
offset += 4; // UnknownValue4
offset += 4; // FirstExecutableFileEntryLength
offset += 4; // MsiFileEntryLength
offset += 4; // UnknownValue7
offset += 4; // UnknownValue8
offset += 4; // ThirdExecutableFileEntryLength
offset += 4; // UnknownValue10
offset += 4; // UnknownValue11
offset += 4; // UnknownValue12
offset += 4; // UnknownValue13
offset += 4; // UnknownValue14
offset += 4; // UnknownValue15
offset += 4; // UnknownValue16
offset += 4; // UnknownValue17
offset += 4; // UnknownValue18
offset += Version?.Length ?? 0;
offset += Model.TmpString == null ? 0 : Model.TmpString.Length + 1;
offset += Model.GuidString == null ? 0 : Model.GuidString.Length + 1;
offset += Model.NonWiseVersion == null ? 0 : Model.NonWiseVersion.Length + 1;
offset += Model.PreFontValue == null ? 0 : Model.PreFontValue.Length;
offset += 4; // FontSize
offset += Model.PreStringValues == null ? 0 : Model.PreStringValues.Length;
if (Model.Strings != null)
{
foreach (var str in Model.Strings)
{
offset += str.Length;
}
}
return offset;
}
}
/// <inheritdoc cref="SectionHeader.UnknownDataSize"/>
public uint UnknownDataSize => Model.UnknownDataSize;
@@ -178,9 +226,8 @@ namespace SabreTools.Serialization.Wrappers
/// <returns>True if the files extracted successfully, false otherwise</returns>
private bool ExtractHeaderDefinedFiles(string outputDirectory, bool includeDebug)
{
// TODO: All reads need to be sequential. At the moment, the data position is at the start of the header;
// TODO: it needs to be wherever the header parser was when it finished. This applies to all logic in the
// TODO: WiseSectionHeader deserializer and wrapper code.
// Seek to the compressed data offset
_dataSource.Seek(CompressedDataOffset, SeekOrigin.Begin);
// Extract first executable, if it exists
if (ExtractFile("FirstExecutable.exe", outputDirectory, FirstExecutableFileEntryLength, includeDebug) != ExtractionStatus.GOOD)

View File

@@ -1,8 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using SabreTools.IO.Extensions;
using SabreTools.IO.Streams;
using SabreTools.Serialization.Interfaces;
@@ -92,141 +89,6 @@ namespace SabreTools.Serialization.Wrappers
#endregion
// TODO: This entire section will be replaced when IO updates
#region Data
/// <summary>
/// Read data from the source
/// </summary>
/// <param name="position">Position in the source to read from</param>
/// <param name="length">Length of the requested data</param>
/// <returns>Byte array containing the requested data, null on error</returns>
/// TODO: This will be replaced with the ReadFrom extension method in IO
public byte[]? ReadFromDataSource(int position, int length)
{
// Validate the requested segment
if (!_dataSource.SegmentValid(position, length))
return null;
try
{
long currentLocation = _dataSource.Position;
_dataSource.Seek(position, SeekOrigin.Begin);
byte[] sectionData = _dataSource.ReadBytes(length);
_dataSource.Seek(currentLocation, SeekOrigin.Begin);
return sectionData;
}
catch
{
// Absorb the error
return null;
}
}
/// <summary>
/// Read string data from the source
/// </summary>
/// <param name="position">Position in the source to read from</param>
/// <param name="length">Length of the requested data</param>
/// <param name="charLimit">Number of characters needed to be a valid string</param>
/// <returns>String list containing the requested data, null on error</returns>
/// TODO: Remove when IO updated
public List<string>? ReadStringsFromDataSource(int position, int length, int charLimit = 5)
{
// Read the data as a byte array first
byte[]? sourceData = ReadFromDataSource(position, length);
if (sourceData == null)
return null;
// Check for ASCII strings
var asciiStrings = ReadStringsWithEncoding(sourceData, charLimit, Encoding.ASCII);
// Check for UTF-8 strings
// We are limiting the check for Unicode characters with a second byte of 0x00 for now
var utf8Strings = ReadStringsWithEncoding(sourceData, charLimit, Encoding.UTF8);
// Check for Unicode strings
// We are limiting the check for Unicode characters with a second byte of 0x00 for now
var unicodeStrings = ReadStringsWithEncoding(sourceData, charLimit, Encoding.Unicode);
// Ignore duplicate strings across encodings
List<string> sourceStrings = [.. asciiStrings, .. utf8Strings, .. unicodeStrings];
// Sort the strings and return
sourceStrings.Sort();
return sourceStrings;
}
/// <summary>
/// Read string data from the source with an encoding
/// </summary>
/// <param name="sourceData">Byte array representing the source data</param>
/// <param name="charLimit">Number of characters needed to be a valid string</param>
/// <param name="encoding">Character encoding to use for checking</param>
/// <returns>String list containing the requested data, empty on error</returns>
/// TODO: Remove when IO updated
#if NET20
private static List<string> ReadStringsWithEncoding(byte[] sourceData, int charLimit, Encoding encoding)
#else
private static HashSet<string> ReadStringsWithEncoding(byte[] sourceData, int charLimit, Encoding encoding)
#endif
{
// Constant from IO
const int MaximumCharactersInString = 64;
if (sourceData == null || sourceData.Length == 0)
return [];
if (charLimit <= 0 || charLimit > sourceData.Length)
return [];
// Create the string set to return
#if NET20
var strings = new List<string>();
#else
var strings = new HashSet<string>();
#endif
// Check for strings
int index = 0;
while (index < sourceData.Length)
{
// Get the maximum number of characters
int maxChars = encoding.GetMaxCharCount(sourceData.Length - index);
int maxBytes = encoding.GetMaxByteCount(Math.Min(MaximumCharactersInString, maxChars));
// Read the longest string allowed
int maxRead = Math.Min(maxBytes, sourceData.Length - index);
string temp = encoding.GetString(sourceData, index, maxRead);
char[] tempArr = temp.ToCharArray();
// Ignore empty strings
if (temp.Length == 0)
{
index++;
continue;
}
// Find the first instance of a control character
int endOfString = Array.FindIndex(tempArr, c => char.IsControl(c) || (c & 0xFF00) != 0);
if (endOfString > -1)
temp = temp.Substring(0, endOfString);
// Otherwise, just add the string if long enough
if (temp.Length >= charLimit)
strings.Add(temp);
// Increment and continue
index += Math.Max(encoding.GetByteCount(temp), 1);
}
return strings;
}
#endregion
#region JSON Export
#if NETCOREAPP

View File

@@ -1,5 +1,6 @@
using System;
using System.IO;
using SabreTools.IO.Extensions;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
@@ -138,7 +139,7 @@ namespace SabreTools.Serialization.Wrappers
return false;
// Load the item data
var data = ReadFromDataSource((int)directoryEntry.EntryOffset, (int)directoryEntry.EntryLength);
var data = _dataSource.ReadFrom((int)directoryEntry.EntryOffset, (int)directoryEntry.EntryLength, retainPosition: true);
if (data == null)
return false;