Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
56d8fc2
Made changes
HeroponRikiBestest Sep 16, 2025
f651740
Temporary hack to not rely on models without significantly changing c…
HeroponRikiBestest Sep 17, 2025
dee16a1
small fixes
HeroponRikiBestest Sep 17, 2025
c3738a9
Store matroschka section as PE extension
HeroponRikiBestest Sep 17, 2025
24a24f8
Move extractor out of deserializer, remove weird hack
HeroponRikiBestest Sep 17, 2025
cce09c9
Potential GA fix
HeroponRikiBestest Sep 17, 2025
254f26a
More potential GA fixes.
HeroponRikiBestest Sep 17, 2025
e4cc377
I have no idea why GA hits that error but not me
HeroponRikiBestest Sep 17, 2025
7bd1900
Giving up on GA for now
HeroponRikiBestest Sep 17, 2025
33112f9
fix locking issues
HeroponRikiBestest Sep 17, 2025
c2d3a07
Fix GA building; thank you sabre
HeroponRikiBestest Sep 18, 2025
0502c6e
Minor improvements all around
HeroponRikiBestest Sep 18, 2025
7999699
Catch some braced single-line if statements
HeroponRikiBestest Sep 18, 2025
6ce4954
Use var more
HeroponRikiBestest Sep 18, 2025
6d01e14
Seperate deserializer into helper methods
HeroponRikiBestest Sep 18, 2025
67b6ed1
Make file path reading much more sane
HeroponRikiBestest Sep 19, 2025
2379b03
Removed MatroschkaHeaderType enum
HeroponRikiBestest Sep 19, 2025
4cf8cbe
Removed MatroschkaGapType enum, further simplify matgaphelper.
HeroponRikiBestest Sep 19, 2025
9eef4a6
Remove MatroschkaHasUnknown enum, further simplify Unknown value read…
HeroponRikiBestest Sep 19, 2025
adeea2a
Cache initial offset.
HeroponRikiBestest Sep 19, 2025
efd7534
Remove TryCreate patterns.
HeroponRikiBestest Sep 19, 2025
8abfbae
Rename matroschka variable to package
HeroponRikiBestest Sep 19, 2025
4e13235
Newline after object
HeroponRikiBestest Sep 19, 2025
6461862
Rename to obj
HeroponRikiBestest Sep 19, 2025
fdd20a6
Remove a few unecessary TODOs
HeroponRikiBestest Sep 19, 2025
d5c118f
Seperate hexstring byte read to another line.
HeroponRikiBestest Sep 19, 2025
c77984f
Fix documentation.
HeroponRikiBestest Sep 19, 2025
3adf499
More private static
HeroponRikiBestest Sep 19, 2025
d11eba1
Changed data.position setting to seeking. NTS: check if this broke an…
HeroponRikiBestest Sep 19, 2025
da8dbf1
rename entries to obj
HeroponRikiBestest Sep 19, 2025
e4795b4
MatroschkaEntry to var
HeroponRikiBestest Sep 19, 2025
13086f4
Newline
HeroponRikiBestest Sep 19, 2025
8254129
Alphabetical
HeroponRikiBestest Sep 19, 2025
9e8d504
More alphabetical.
HeroponRikiBestest Sep 19, 2025
2d4ea64
section to package
HeroponRikiBestest Sep 19, 2025
561b2de
Move private variables.
HeroponRikiBestest Sep 19, 2025
eecd9d1
Move to extension properties.
HeroponRikiBestest Sep 19, 2025
e581cc6
Revert section finding.
HeroponRikiBestest Sep 19, 2025
45c0cd0
Remove uneeded _dataSource lock and access.
HeroponRikiBestest Sep 19, 2025
1bcf0c5
combine lines and make var
HeroponRikiBestest Sep 19, 2025
fa1de8b
Combine two null checks.
HeroponRikiBestest Sep 19, 2025
c02c080
Packaged files, some past commits I think I forgot to push.
HeroponRikiBestest Sep 19, 2025
e999ac9
Missed two
HeroponRikiBestest Sep 19, 2025
00d35bd
newline
HeroponRikiBestest Sep 19, 2025
ae30fdf
space
HeroponRikiBestest Sep 19, 2025
8e54c08
newline
HeroponRikiBestest Sep 19, 2025
7d5046a
Combine two lines
HeroponRikiBestest Sep 19, 2025
b445af1
Removed comment
HeroponRikiBestest Sep 19, 2025
e0b76fc
Return false explicitly
HeroponRikiBestest Sep 19, 2025
f6ad633
Change hashing string implementation
HeroponRikiBestest Sep 19, 2025
afde07e
Fix order.
HeroponRikiBestest Sep 19, 2025
53f0313
Use offset reading instead of filedataarray
HeroponRikiBestest Sep 19, 2025
581a96c
Change file reading around a little preemptively for BOS
HeroponRikiBestest Sep 19, 2025
51b06fd
Merge branch 'main' into matroschka-changes
mnadareski Sep 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions SabreTools.Serialization/Deserializers/SecuROMMatroschkaPackage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
using System.IO;
using System.Text;
using SabreTools.IO.Extensions;
using SabreTools.Matching;
using SabreTools.Models.SecuROM;
using static SabreTools.Models.SecuROM.Constants;

namespace SabreTools.Serialization.Deserializers
{
public class SecuROMMatroschkaPackage : BaseBinaryDeserializer<MatroshkaPackage>
{
/// <inheritdoc/>
public override MatroshkaPackage? Deserialize(Stream? data)
{
// If the data is invalid
if (data == null || !data.CanRead)
return null;

try
{
// 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);
if (package == null)
return null;

var entries = ParseEntries(data, package);
if (entries == null)
return null;

package.Entries = entries;
return package;
}
catch
{
// Ignore the actual error
return null;
}
}

private static MatroshkaPackage? ParsePreEntryHeader(Stream data)
{
var obj = new MatroshkaPackage();

byte[] magic = data.ReadBytes(4);
obj.Signature = Encoding.ASCII.GetString(magic);
if (obj.Signature != MatroshkaMagicString)
return null;

obj.EntryCount = data.ReadUInt32LittleEndian();
if (obj.EntryCount == 0)
return null;

// Check if "matrosch" section is a longer header one or not based on whether the next uint is 0 or 1. Anything
// else will just already be starting the filename string, which is never going to start with this.
// Previously thought that the longer header was correlated with RC, but at least one executable
// (NecroVisioN.exe from the GamersGate patch NecroVisioN_Patch1.2_GG.exe) isn't RC and still has it.
long tempPosition = data.Position;
uint tempValue = data.ReadUInt32LittleEndian();
data.Seek(tempPosition, SeekOrigin.Begin);

if (tempValue < 2) // Only little-endian 0 or 1 have been observed for long sections.
{
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)
{

// If we have any entries
var obj = new MatroshkaEntry[package.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;
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;

// Entry does have the unknown value.
return true;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@ public partial class PortableExecutable : IExtractable
public bool Extract(string outputDirectory, bool includeDebug)
{
bool cexe = ExtractCExe(outputDirectory, includeDebug);
bool matroschka = ExtractMatroschka(outputDirectory, includeDebug);
bool overlay = ExtractFromOverlay(outputDirectory, includeDebug);
bool resources = ExtractFromResources(outputDirectory, includeDebug);
bool wise = ExtractWise(outputDirectory, includeDebug);

return cexe || overlay || resources | wise;
return cexe || matroschka || overlay || resources || wise;
}

/// <summary>
Expand Down Expand Up @@ -373,6 +374,22 @@ public bool ExtractFromResources(string outputDirectory, bool includeDebug)
return false;
}
}

/// <summary>
/// Extract data from a SecuROM Matroschka Package
/// </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 ExtractMatroschka(string outputDirectory, bool includeDebug)
{
// Check if executable contains Matroschka package or not
if (MatroschkaPackage == null)
return false;

// Attempt to extract package
return MatroschkaPackage.Extract(outputDirectory, includeDebug);
}

/// <summary>
/// Extract data from a Wise installer
Expand Down
117 changes: 117 additions & 0 deletions SabreTools.Serialization/Wrappers/PortableExecutable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,84 @@ public List<string> HeaderPaddingStrings

/// <inheritdoc cref="Executable.ImportTable"/>
public ImportTable? ImportTable => Model.ImportTable;

public SecuROMMatroschkaPackage? MatroschkaPackage
{
get
{
lock (_matroschkaPackageLock)
{
// 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)
{
_matroschkaPackageFailed = true;
return null;
}

// If the section table is missing
if (SectionTable == null)
{
_matroschkaPackageFailed = true;
return null;
}

SectionHeader? section = null;

// Find the matrosch or rcpacker section
foreach (var searchedSection in SectionTable)
{
string sectionName = Encoding.ASCII.GetString(searchedSection.Name ?? []).TrimEnd('\0');
if (sectionName != "matrosch" && sectionName != "rcpacker")
continue;

section = searchedSection;
break;
}

// Otherwise, it could not be found
if (section == null)
{
_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)
{
_matroschkaPackageFailed = true;
return null;
}

// Otherwise, cache and return the data
_matroschkaPackage = header;
return _matroschkaPackage;
}
}
}

/// <inheritdoc cref="Executable.OptionalHeader"/>
public OptionalHeader? OptionalHeader => Model.OptionalHeader;
Expand Down Expand Up @@ -784,7 +862,22 @@ public string? AssemblyVersion
/// Lock object for <see cref="_headerPaddingStrings"/>
/// </summary>
private readonly object _headerPaddingStringsLock = new();

/// <summary>
/// Matroschka Package wrapper, if it exists
/// </summary>
private SecuROMMatroschkaPackage? _matroschkaPackage = null;

/// <summary>
/// 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>
Expand Down Expand Up @@ -1526,6 +1619,30 @@ public long FindWiseOverlayHeader()
}
}

/// <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
Expand Down
Loading