Minor cleanup to previous commit

This commit is contained in:
Matt Nadareski
2025-09-20 10:32:53 -04:00
parent fbdadce129
commit 8f64e2defd
5 changed files with 180 additions and 203 deletions

View File

@@ -10,6 +10,7 @@ namespace SabreTools.Serialization.Deserializers
public class SecuROMMatroschkaPackage : BaseBinaryDeserializer<MatroshkaPackage>
{
/// <inheritdoc/>
/// TODO: Unify matroschka spelling to "Matroschka"
public override MatroshkaPackage? Deserialize(Stream? data)
{
// If the data is invalid
@@ -20,18 +21,15 @@ namespace SabreTools.Serialization.Deserializers
{
// Cache the initial offset
long initialOffset = data.Position;
// TODO: Unify matroschka spelling. They spell it matroschka in all official stuff, as far as has been observed. Will double check.
// Try to parse the header
var package = ParsePreEntryHeader(data);
var package = ParseMatroshkaPackage(data);
if (package == null)
return null;
var entries = ParseEntries(data, package);
if (entries == null)
return null;
package.Entries = entries;
// Try to parse the entries
package.Entries = ParseEntries(data, package.EntryCount);
return package;
}
catch
@@ -41,10 +39,15 @@ namespace SabreTools.Serialization.Deserializers
}
}
private static MatroshkaPackage? ParsePreEntryHeader(Stream data)
{
/// <summary>
/// Parse a Stream into a MatroshkaPackage
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled MatroshkaPackage on success, null on error</returns>
public static MatroshkaPackage? ParseMatroshkaPackage(Stream data)
{
var obj = new MatroshkaPackage();
byte[] magic = data.ReadBytes(4);
obj.Signature = Encoding.ASCII.GetString(magic);
if (obj.Signature != MatroshkaMagicString)
@@ -62,88 +65,74 @@ namespace SabreTools.Serialization.Deserializers
uint tempValue = data.ReadUInt32LittleEndian();
data.Seek(tempPosition, SeekOrigin.Begin);
if (tempValue < 2) // Only little-endian 0 or 1 have been observed for long sections.
// Only 0 or 1 have been observed for long sections
if (tempValue < 2)
{
obj.UnknownRCValue1 = data.ReadUInt32LittleEndian();
obj.UnknownRCValue2 = data.ReadUInt32LittleEndian();
obj.UnknownRCValue3 = data.ReadUInt32LittleEndian();
// Exact byte count has to be used because non-RC executables have all 0x00 here.
var keyHexBytes = data.ReadBytes(32);
obj.KeyHexString = Encoding.ASCII.GetString(keyHexBytes);
if (!data.ReadBytes(4).EqualsExactly([0x00, 0x00, 0x00, 0x00]))
return null;
}
return obj;
}
private static MatroshkaEntry[]? ParseEntries(Stream data, MatroshkaPackage package)
/// <summary>
/// Parse a Stream into a MatroshkaEntry array
/// </summary>
/// <param name="data">Stream to parse</param>
/// <param name="entryCount">Number of entries in the array</param>
/// <returns>Filled MatroshkaEntry array on success, null on error</returns>
private static MatroshkaEntry[] ParseEntries(Stream data, uint entryCount)
{
// If we have any entries
var obj = new MatroshkaEntry[package.EntryCount];
var obj = new MatroshkaEntry[entryCount];
int matGapType = 0;
bool? matHasUnknown = null;
// Read entries
for (int i = 0; i < obj.Length; i++)
{
var entry = new MatroshkaEntry();
// Determine if file path size is 256 or 512 bytes
if (matGapType == 0)
matGapType = GapHelper(data);
// TODO: Spaces/non-ASCII have not yet been observed. Still, probably safer to store as byte array?
// TODO: Read as string and trim once models is bumped. For now, this needs to be trimmed by anything reading it.
entry.Path = data.ReadBytes((int)matGapType);
// Entry type isn't currently validated as it's always predictable anyways, nor necessary to know.
entry.EntryType = (MatroshkaEntryType)data.ReadUInt32LittleEndian();
entry.Size = data.ReadUInt32LittleEndian();
entry.Offset = data.ReadUInt32LittleEndian();
// Check for unknown 4-byte 0x00 value. Not correlated with 256 vs 512-byte gaps.
if (matHasUnknown == null)
matHasUnknown = UnknownHelper(data, entry);
if (matHasUnknown == true) // If already known, read or don't read the unknown value.
entry.Unknown = data.ReadUInt32LittleEndian(); // TODO: Validate it's zero?
entry.ModifiedTime = data.ReadUInt64LittleEndian();
entry.CreatedTime = data.ReadUInt64LittleEndian();
entry.AccessedTime = data.ReadUInt64LittleEndian();
entry.MD5 = data.ReadBytes(16);
obj[i] = entry;
}
return obj;
}
private static int GapHelper(Stream data)
{
var tempPosition = data.Position;
// Determine if file path size is 256 or 512 bytes
long tempPosition = data.Position;
data.Seek(data.Position + 256, SeekOrigin.Begin);
var tempValue = data.ReadUInt32LittleEndian();
data.Seek(tempPosition, SeekOrigin.Begin);
if (tempValue <= 0) // Gap is 512 bytes. Actually just == 0, but ST prefers ranges.
return 512;
// Otherwise, gap is 256 bytes.
return 256;
}
private static bool UnknownHelper(Stream data, MatroshkaEntry entry)
{
var tempPosition = data.Position;
var tempValue = data.ReadUInt32LittleEndian();
data.Seek(tempPosition, SeekOrigin.Begin);
if (tempValue > 0) // Entry does not have the Unknown value.
return false;
int gapSize = tempValue == 0 ? 512 : 256;
// Entry does have the unknown value.
return true;
// Set default value for unknown value checking
bool? hasUnknown = null;
// Read entries
for (int i = 0; i < obj.Length; i++)
{
var entry = new MatroshkaEntry();
entry.Path = data.ReadBytes(gapSize);
entry.EntryType = (MatroshkaEntryType)data.ReadUInt32LittleEndian();
entry.Size = data.ReadUInt32LittleEndian();
entry.Offset = data.ReadUInt32LittleEndian();
// On the first entry, determine if the unknown value exists
if (hasUnknown == null)
{
tempPosition = data.Position;
tempValue = data.ReadUInt32LittleEndian();
data.Seek(tempPosition, SeekOrigin.Begin);
hasUnknown = tempValue == 0;
}
// TODO: Validate it's zero?
if (hasUnknown == true)
entry.Unknown = data.ReadUInt32LittleEndian();
entry.ModifiedTime = data.ReadUInt64LittleEndian();
entry.CreatedTime = data.ReadUInt64LittleEndian();
entry.AccessedTime = data.ReadUInt64LittleEndian();
entry.MD5 = data.ReadBytes(16);
obj[i] = entry;
}
return obj;
}
}
}

View File

@@ -15,6 +15,7 @@ namespace SabreTools.Serialization.Wrappers
/// - Archives and executables in the overlay
/// - Archives and executables in resource data
/// - CExe-compressed resource data
/// - SecuROM Matroschka package sections
/// - SFX archives (7z, MS-CAB, PKZIP, RAR)
/// - Wise installers
/// </remarks>

View File

@@ -184,7 +184,10 @@ namespace SabreTools.Serialization.Wrappers
/// <inheritdoc cref="Executable.ImportTable"/>
public ImportTable? ImportTable => Model.ImportTable;
/// <summary>
/// SecuROM Matroschka package wrapper, if it exists
/// </summary>
public SecuROMMatroschkaPackage? MatroschkaPackage
{
get
@@ -194,11 +197,11 @@ namespace SabreTools.Serialization.Wrappers
// Use the cached data if possible
if (_matroschkaPackage != null)
return _matroschkaPackage;
// Check to see if creation has already been attempted
if (_matroschkaPackageFailed)
return null;
// Get the available source length, if possible
var dataLength = Length;
if (dataLength == -1)
@@ -213,10 +216,9 @@ namespace SabreTools.Serialization.Wrappers
_matroschkaPackageFailed = true;
return null;
}
SectionHeader? section = null;
// Find the matrosch or rcpacker section
SectionHeader? section = null;
foreach (var searchedSection in SectionTable)
{
string sectionName = Encoding.ASCII.GetString(searchedSection.Name ?? []).TrimEnd('\0');
@@ -233,31 +235,29 @@ namespace SabreTools.Serialization.Wrappers
_matroschkaPackageFailed = true;
return null;
}
// Get the offset
long offset = section.VirtualAddress.ConvertVirtualAddress(SectionTable);
if (offset < 0 || offset >= Length)
{
_matroschkaPackageFailed = true;
return null;
}
}
// Read the section into a local array
var sectionLength = (int)section.VirtualSize;
var sectionData = ReadRangeFromSource(offset, sectionLength);
// Parse the section header
var header = SecuROMMatroschkaPackage.Create(sectionData, 0);
// If header creation failed, or if Entries does not exist
if (header?.Entries == null)
if (sectionData.Length == 0)
{
_matroschkaPackageFailed = true;
return null;
}
// Otherwise, cache and return the data
_matroschkaPackage = header;
// Parse the package
_matroschkaPackage = SecuROMMatroschkaPackage.Create(sectionData, 0);
if (_matroschkaPackage?.Entries == null)
_matroschkaPackageFailed = true;
return _matroschkaPackage;
}
}
@@ -622,7 +622,12 @@ namespace SabreTools.Serialization.Wrappers
// Read the section into a local array
int sectionLength = (int)wiseSection.VirtualSize;
byte[]? sectionData = ReadRangeFromSource(offset, sectionLength);
byte[] sectionData = ReadRangeFromSource(offset, sectionLength);
if (sectionData.Length == 0)
{
_wiseSectionHeaderMissing = true;
return null;
}
// Parse the section header
_wiseSectionHeader = WiseSectionHeader.Create(sectionData, 0);
@@ -862,7 +867,7 @@ namespace SabreTools.Serialization.Wrappers
/// Lock object for <see cref="_headerPaddingStrings"/>
/// </summary>
private readonly object _headerPaddingStringsLock = new();
/// <summary>
/// Matroschka Package wrapper, if it exists
/// </summary>
@@ -872,12 +877,12 @@ namespace SabreTools.Serialization.Wrappers
/// Lock object for <see cref="_matroschkaPackage"/>
/// </summary>
private readonly object _matroschkaPackageLock = new();
/// <summary>
/// Cached attempt at creation for <see cref="_matroschkaPackage"/>
/// </summary>
private bool _matroschkaPackageFailed = false;
/// <summary>
/// Address of the overlay, if it exists
/// </summary>
@@ -1619,30 +1624,6 @@ namespace SabreTools.Serialization.Wrappers
}
}
/// <summary>
/// Find the location of a SecuROM Matroschka section, if it exists
/// </summary>
/// <returns>Matroschka section on success, null otherwise</returns>
public Models.PortableExecutable.SectionHeader? FindMatroschkaSection()
{
// If the section table is invalid
if (SectionTable == null)
return null;
// Find the matrosch or rcpacker section
foreach (var section in SectionTable)
{
var sectionName = Encoding.ASCII.GetString(section.Name ?? []).TrimEnd('\0');
if (sectionName != "matrosch" && sectionName != "rcpacker")
continue;
return section;
}
// Otherwise, it could not be found
return null;
}
#endregion
#region Resource Parsing

View File

@@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Text;
using SabreTools.Hashing;
using SabreTools.Models.SecuROM;
using SabreTools.Serialization.Interfaces;
@@ -11,59 +12,44 @@ namespace SabreTools.Serialization.Wrappers
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// Extract the packaged files
var extracted = ExtractPackagedFiles(outputDirectory, includeDebug);
if (!extracted)
{
if (includeDebug) Console.Error.WriteLine("Could not extract packaged files");
// If we have no entries
if (Entries == null || Entries.Length == 0)
return false;
// Loop through and extract all files to the output
bool allExtracted = true;
for (var i = 0; i < Entries.Length; i++)
{
allExtracted &= ExtractFile(i, outputDirectory, includeDebug);
}
return true;
return allExtracted;
}
/// <summary>
/// Extract the packaged files.
/// Extract a file from the package to an output directory by index
/// </summary>
/// <param name="index">File index to extract</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 ExtractPackagedFiles(string outputDirectory, bool includeDebug)
/// <returns>True if the file extracted, false otherwise</returns>
public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
{
if (Entries == null)
return false;
var successful = true;
// If we have no entries
if (Entries == null || Entries.Length == 0)
return false;
// Extract entries
for (var i = 0; i < Entries.Length; i++)
{
var entry = Entries[i];
// Extract file
if (!ExtractFile(entry, outputDirectory, includeDebug))
successful = false;
}
return successful;
}
// If the entry index is invalid
if (index < 0 || index >= Entries.Length)
return false;
/// <summary>
/// Attempt to extract a file
/// </summary>
/// <param name="entry">Matroschka file entry being extracted</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>Boolean representing true on success or false on failure</returns>
/// <remarks>Assumes that the current stream position is the end of where the data lives</remarks>
private bool ExtractFile(MatroshkaEntry entry, string outputDirectory, bool includeDebug)
{
// Get the entry
var entry = Entries[index];
if (entry.Path == null)
return false;
var filename = System.Text.Encoding.ASCII.GetString(entry.Path).TrimEnd('\0');
// Ensure directory separators are consistent
string filename = Encoding.ASCII.GetString(entry.Path).TrimEnd('\0');
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
@@ -72,9 +58,8 @@ namespace SabreTools.Serialization.Wrappers
if (includeDebug) Console.WriteLine($"Attempting to extract {filename}");
// Read the file
var fileData = ReadFile(entry, includeDebug);
if (fileData == null)
var data = ReadFile(entry, includeDebug);
if (data == null)
return false;
// Ensure the full output directory exists
@@ -83,47 +68,63 @@ namespace SabreTools.Serialization.Wrappers
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Write the output file
File.WriteAllBytes(filename, fileData);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = File.OpenWrite(filename);
fs.Write(data, 0, data.Length);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
/// <summary>
/// Read file and check bytes to be extracted against MD5 checksum.
/// Read file and check bytes to be extracted against MD5 checksum
/// </summary>
/// <param name="entry">Entry being extracted</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>Byte array of the file data if successful, null if unsuccessful.</returns>
public byte[]? ReadFile(MatroshkaEntry entry, bool includeDebug)
/// <returns>Byte array of the file data if successful, null otherwise</returns>
private byte[]? ReadFile(MatroshkaEntry entry, bool includeDebug)
{
var fileData = ReadRangeFromSource(entry.Offset, (int)entry.Size); // TODO: safety? validation? anything?
// Debug output
if (includeDebug) Console.WriteLine($"Offset: {entry.Offset:X8}, Expected Size: {entry.Size}");
// Skip if the entry is incomplete
if (entry.Path == null || entry.MD5 == null)
return null;
string expectedMd5 = BitConverter.ToString(entry.MD5!);
// Cache the expected MD5
string expectedMd5 = BitConverter.ToString(entry.MD5);
expectedMd5 = expectedMd5.ToLowerInvariant().Replace("-", string.Empty);
// Debug output
if (includeDebug) Console.WriteLine($"Expected MD5: {expectedMd5}");
if (includeDebug) Console.WriteLine($"Offset: {entry.Offset:X8}, Expected Size: {entry.Size}, Expected MD5: {expectedMd5}");
if (fileData == null)
return null;
var hashBytes = HashTool.GetByteArrayHashArray(fileData, HashType.MD5);
string actualMd5 = BitConverter.ToString(hashBytes!);
actualMd5 = actualMd5.ToLowerInvariant().Replace("-", string.Empty);
// Debug output
if (includeDebug) Console.WriteLine($"Actual MD5: {actualMd5}");
if (hashBytes == null || actualMd5 != expectedMd5)
// Attempt to read from the offset
var fileData = ReadRangeFromSource(entry.Offset, (int)entry.Size);
if (fileData.Length == 0)
{
var filename = System.Text.Encoding.ASCII.GetString(entry.Path!).TrimEnd('\0');
Console.Error.WriteLine($"MD5 checksum failure for file {filename})");
if (includeDebug) Console.Error.WriteLine($"Could not read {entry.Size} bytes from {entry.Offset:X8}");
return null;
}
// Get the actual MD5 of the data
string actualMd5 = HashTool.GetByteArrayHash(fileData, HashType.MD5) ?? string.Empty;
// Debug output
if (includeDebug) Console.WriteLine($"Actual MD5: {actualMd5}");
// Do not return on a hash mismatch
if (actualMd5 != expectedMd5)
{
string filename = Encoding.ASCII.GetString(entry.Path).TrimEnd('\0');
if (includeDebug) Console.Error.WriteLine($"MD5 checksum failure for file {filename})");
return null;
}
return fileData;
}

View File

@@ -38,32 +38,38 @@ namespace SabreTools.Serialization.Wrappers
/// <inheritdoc cref="MatroshkaPackage.Entries"/>
public MatroshkaEntry[]? Entries => Model.Entries;
// TODO: Use entries from model after models update.
#endregion
#region Constructors
/// <inheritdoc/>
public SecuROMMatroschkaPackage(MatroshkaPackage model, byte[] data, int offset)
: base(model, data, offset)
{
// All logic is handled by the base class
}
public SecuROMMatroschkaPackage(MatroshkaPackage model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public SecuROMMatroschkaPackage(MatroshkaPackage model, Stream data)
: base(model, data)
{
// All logic is handled by the base class
}
public SecuROMMatroschkaPackage(MatroshkaPackage model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public SecuROMMatroschkaPackage(MatroshkaPackage model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public SecuROMMatroschkaPackage(MatroshkaPackage model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public SecuROMMatroschkaPackage(MatroshkaPackage model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public SecuROMMatroschkaPackage(MatroshkaPackage model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create a SecuROM Matroschka Package section from a byte array and offset
/// Create a SecuROM Matroschka package from a byte array and offset
/// </summary>
/// <param name="data">Byte array representing the section</param>
/// <param name="data">Byte array representing the package</param>
/// <param name="offset">Offset within the array to parse</param>
/// <returns>A SecuROM Matroschka Package section wrapper on success, null on failure</returns>
/// <returns>A SecuROM Matroschka package wrapper on success, null on failure</returns>
public static SecuROMMatroschkaPackage? Create(byte[]? data, int offset)
{
// If the data is invalid
@@ -80,10 +86,10 @@ namespace SabreTools.Serialization.Wrappers
}
/// <summary>
/// Create a SecuROM Matroschka Package section from a Stream
/// Create a SecuROM Matroschka package from a Stream
/// </summary>
/// <param name="data">Stream representing the section</param>
/// <returns>A SecuROM Matroschka Package section wrapper on success, null on failure</returns>
/// <param name="data">Stream representing the package</param>
/// <returns>A SecuROM Matroschka package wrapper on success, null on failure</returns>
public static SecuROMMatroschkaPackage? Create(Stream? data)
{
// If the data is invalid
@@ -99,8 +105,7 @@ namespace SabreTools.Serialization.Wrappers
if (model == null)
return null;
data.Seek(currentOffset, SeekOrigin.Begin);
return new SecuROMMatroschkaPackage(model, data);
return new SecuROMMatroschkaPackage(model, data, currentOffset);
}
catch
{