From 8f64e2defd509b626c05b4b932f523db79bbd537 Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Sat, 20 Sep 2025 10:32:53 -0400 Subject: [PATCH] Minor cleanup to previous commit --- .../Deserializers/SecuROMMatroschkaPackage.cs | 137 ++++++++---------- .../Wrappers/PortableExecutable.Extraction.cs | 1 + .../Wrappers/PortableExecutable.cs | 71 ++++----- .../SecuROMMatroschkaPackage.Extraction.cs | 129 +++++++++-------- .../Wrappers/SecuROMMatroschkaPackage.cs | 45 +++--- 5 files changed, 180 insertions(+), 203 deletions(-) diff --git a/SabreTools.Serialization/Deserializers/SecuROMMatroschkaPackage.cs b/SabreTools.Serialization/Deserializers/SecuROMMatroschkaPackage.cs index bca5c271..2ba4eec8 100644 --- a/SabreTools.Serialization/Deserializers/SecuROMMatroschkaPackage.cs +++ b/SabreTools.Serialization/Deserializers/SecuROMMatroschkaPackage.cs @@ -10,6 +10,7 @@ namespace SabreTools.Serialization.Deserializers public class SecuROMMatroschkaPackage : BaseBinaryDeserializer { /// + /// 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) - { + /// + /// Parse a Stream into a MatroshkaPackage + /// + /// Stream to parse + /// Filled MatroshkaPackage on success, null on error + 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) + /// + /// Parse a Stream into a MatroshkaEntry array + /// + /// Stream to parse + /// Number of entries in the array + /// Filled MatroshkaEntry array on success, null on error + 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; } } } \ No newline at end of file diff --git a/SabreTools.Serialization/Wrappers/PortableExecutable.Extraction.cs b/SabreTools.Serialization/Wrappers/PortableExecutable.Extraction.cs index 7ecf8cb2..3d8e3c10 100644 --- a/SabreTools.Serialization/Wrappers/PortableExecutable.Extraction.cs +++ b/SabreTools.Serialization/Wrappers/PortableExecutable.Extraction.cs @@ -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 /// diff --git a/SabreTools.Serialization/Wrappers/PortableExecutable.cs b/SabreTools.Serialization/Wrappers/PortableExecutable.cs index 522505e3..48b6bcaa 100644 --- a/SabreTools.Serialization/Wrappers/PortableExecutable.cs +++ b/SabreTools.Serialization/Wrappers/PortableExecutable.cs @@ -184,7 +184,10 @@ namespace SabreTools.Serialization.Wrappers /// public ImportTable? ImportTable => Model.ImportTable; - + + /// + /// SecuROM Matroschka package wrapper, if it exists + /// 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 /// private readonly object _headerPaddingStringsLock = new(); - + /// /// Matroschka Package wrapper, if it exists /// @@ -872,12 +877,12 @@ namespace SabreTools.Serialization.Wrappers /// Lock object for /// private readonly object _matroschkaPackageLock = new(); - + /// /// Cached attempt at creation for /// private bool _matroschkaPackageFailed = false; - + /// /// Address of the overlay, if it exists /// @@ -1619,30 +1624,6 @@ namespace SabreTools.Serialization.Wrappers } } - /// - /// Find the location of a SecuROM Matroschka section, if it exists - /// - /// Matroschka section on success, null otherwise - 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 diff --git a/SabreTools.Serialization/Wrappers/SecuROMMatroschkaPackage.Extraction.cs b/SabreTools.Serialization/Wrappers/SecuROMMatroschkaPackage.Extraction.cs index 533a5193..47b6a31e 100644 --- a/SabreTools.Serialization/Wrappers/SecuROMMatroschkaPackage.Extraction.cs +++ b/SabreTools.Serialization/Wrappers/SecuROMMatroschkaPackage.Extraction.cs @@ -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 /// 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; } /// - /// Extract the packaged files. + /// Extract a file from the package to an output directory by index /// + /// File index to extract /// Output directory to write to /// True to include debug data, false otherwise - /// True if the files extracted successfully, false otherwise - private bool ExtractPackagedFiles(string outputDirectory, bool includeDebug) + /// True if the file extracted, false otherwise + 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; - /// - /// Attempt to extract a file - /// - /// Matroschka file entry being extracted - /// Output directory to write to - /// True to include debug data, false otherwise - /// Boolean representing true on success or false on failure - /// Assumes that the current stream position is the end of where the data lives - 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; } /// - /// Read file and check bytes to be extracted against MD5 checksum. + /// Read file and check bytes to be extracted against MD5 checksum /// /// Entry being extracted /// True to include debug data, false otherwise - /// Byte array of the file data if successful, null if unsuccessful. - public byte[]? ReadFile(MatroshkaEntry entry, bool includeDebug) + /// Byte array of the file data if successful, null otherwise + 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; } diff --git a/SabreTools.Serialization/Wrappers/SecuROMMatroschkaPackage.cs b/SabreTools.Serialization/Wrappers/SecuROMMatroschkaPackage.cs index 8f30e173..39233943 100644 --- a/SabreTools.Serialization/Wrappers/SecuROMMatroschkaPackage.cs +++ b/SabreTools.Serialization/Wrappers/SecuROMMatroschkaPackage.cs @@ -38,32 +38,38 @@ namespace SabreTools.Serialization.Wrappers /// public MatroshkaEntry[]? Entries => Model.Entries; - // TODO: Use entries from model after models update. - #endregion #region Constructors /// - 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) { } /// - 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) { } + + /// + public SecuROMMatroschkaPackage(MatroshkaPackage model, byte[] data, int offset, int length) : base(model, data, offset, length) { } + + /// + public SecuROMMatroschkaPackage(MatroshkaPackage model, Stream data) : base(model, data) { } + + /// + public SecuROMMatroschkaPackage(MatroshkaPackage model, Stream data, long offset) : base(model, data, offset) { } + + /// + public SecuROMMatroschkaPackage(MatroshkaPackage model, Stream data, long offset, long length) : base(model, data, offset, length) { } + + #endregion + + #region Static Constructors /// - /// Create a SecuROM Matroschka Package section from a byte array and offset + /// Create a SecuROM Matroschka package from a byte array and offset /// - /// Byte array representing the section + /// Byte array representing the package /// Offset within the array to parse - /// A SecuROM Matroschka Package section wrapper on success, null on failure + /// A SecuROM Matroschka package wrapper on success, null on failure public static SecuROMMatroschkaPackage? Create(byte[]? data, int offset) { // If the data is invalid @@ -80,10 +86,10 @@ namespace SabreTools.Serialization.Wrappers } /// - /// Create a SecuROM Matroschka Package section from a Stream + /// Create a SecuROM Matroschka package from a Stream /// - /// Stream representing the section - /// A SecuROM Matroschka Package section wrapper on success, null on failure + /// Stream representing the package + /// A SecuROM Matroschka package wrapper on success, null on failure 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 {