Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .github/workflows/check_pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- name: Setup .NET
uses: actions/setup-dotnet@v4
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace SabreTools.Data.Models.InstallShieldExecutable
{
public class ExtractableFile
{
/// <summary>
/// Name of the file, only ASCII characters(?)
/// </summary>
public string? Name { get; set; }

/// <summary>
/// Path of the file, only ASCII characters(?), seems to usually use \ filepaths
/// </summary>
public string? Path { get; set; }

/// <summary>
/// Version of the file
/// </summary>
public string? Version { get; set; }

/// <summary>
/// Length of the file. Stored in the installshield executable as a string.
/// </summary>
public ulong Length { get; set; }
}
}
66 changes: 66 additions & 0 deletions SabreTools.Serialization/Readers/InstallShieldExecutableFile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using System.IO;
using SabreTools.Data.Models.InstallShieldExecutable;
using SabreTools.IO.Extensions;

namespace SabreTools.Serialization.Readers
{
public class InstallShieldExecutableFile : BaseBinaryReader<ExtractableFile>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Future note: This will likely be expanded to cover an entire overlay section and not a singular entry.

Copy link
Contributor Author

@HeroponRikiBestest HeroponRikiBestest Oct 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will I need to do that? I'd be fine with doing it if I understood how it would be expanded to cover that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not in this PR

{
public override ExtractableFile? Deserialize(Stream? data)
{
// If the data is invalid
if (data == null || !data.CanRead)
return null;

try
{
// Cache the initial offset
long initialOffset = data.Position;

// Try to parse the header
var header = ParseExtractableFileHeader(data);
if (header == null)
return null;

return header;
}
catch
{
// Ignore the actual error
return null;
}
}

/// <summary>
/// Parse a Stream into an InstallShield Executable file header
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled InstallShield Executable file header on success, null on error</returns>
public static ExtractableFile? ParseExtractableFileHeader(Stream? data)
{
var obj = new ExtractableFile();

obj.Name = data.ReadNullTerminatedAnsiString();
if (obj.Name == null)
return null;

obj.Path = data.ReadNullTerminatedAnsiString();
if (obj.Path == null)
return null;

obj.Version = data.ReadNullTerminatedAnsiString();
if (obj.Version == null)
return null;

var versionString = data.ReadNullTerminatedAnsiString();
if (versionString == null || !ulong.TryParse(versionString, out var foundVersion))
return null;

obj.Length = foundVersion;
if (obj.Name == null)
return null;

return obj;
}
}
}
85 changes: 83 additions & 2 deletions SabreTools.Serialization/Wrappers/PortableExecutable.Extraction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public partial class PortableExecutable : IExtractable
/// - SFX archives
/// + 7z
/// + Advanced Installer
/// + InstallShield Executables
/// + MS-CAB
/// + PKZIP
/// + RAR
Expand All @@ -28,6 +29,7 @@ public bool Extract(string outputDirectory, bool includeDebug)
{
bool cai = ExtractAdvancedInstaller(outputDirectory, includeDebug);
bool cexe = ExtractCExe(outputDirectory, includeDebug);
bool issexe = ExtractInstallShieldExecutable(outputDirectory, includeDebug);
bool matroschka = ExtractMatroschka(outputDirectory, includeDebug);
bool resources = ExtractFromResources(outputDirectory, includeDebug);
bool spoon = ExtractSpoonInstaller(outputDirectory, includeDebug);
Expand All @@ -37,10 +39,10 @@ public bool Extract(string outputDirectory, bool includeDebug)
bool wiseSection = wiseOverlay || ExtractWiseSection(outputDirectory, includeDebug);

// Overlay can be skipped in some situations
bool overlay = cai || spoon || wiseOverlay
bool overlay = cai || issexe || spoon || wiseOverlay
|| ExtractFromOverlay(outputDirectory, includeDebug);

return cai || cexe || matroschka || overlay || resources || spoon
return cai || cexe || issexe || matroschka || overlay || resources || spoon
|| wiseOverlay || wiseSection;
}

Expand Down Expand Up @@ -158,6 +160,85 @@ public bool ExtractCExe(string outputDirectory, bool includeDebug)
return false;
}
}

/// <summary>
/// Extract data from an InstallShield Executable
/// </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 ExtractInstallShieldExecutable(string outputDirectory, bool includeDebug)
{
try
{
long overlayAddress = OverlayAddress;

// Return if overlay doesn't exist.
if (overlayAddress == -1)
return false;

// Ensure the stream is starting at the overlay address
_dataSource.Seek(overlayAddress, SeekOrigin.Begin);

var streamLength = _dataSource.Length;
const int chunkSize = 65536;
var deserializer = new Readers.InstallShieldExecutableFile();

while (_dataSource.Position < streamLength)
{
lock (_dataSourceLock)
{
// Try to deserialize the source data

var entry = deserializer.Deserialize(_dataSource);
if (entry?.Path == null)
return false;

// Get the length, and make sure it won't EOF
var length = (long)entry.Length;
if (length > streamLength - _dataSource.Position)
break;

// Ensure directory separators are consistent
// Path is used instead of Name because Path contains the filename anyways.

var filename = entry.Path.TrimEnd('\0');
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');

// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);

// Write the output file
using var fs = File.Open(filename, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
Console.WriteLine($"Attempting to extract {entry.Name} from potential InstallShield Executable");

// Read from file in chunks in order to save memory, since some extracted files will be large
// Chunk size is purely arbitrary and can be adjusted as needed.
// Read file from InstallShield Executable and write it as an output file.
while (length > 0)
{
var bytesToRead = (int)Math.Min(length, chunkSize);
var buffer = _dataSource.ReadBytes(bytesToRead);
fs.Write(buffer, 0, bytesToRead);
fs.Flush();
length -= bytesToRead;
}
}
}
return true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}

/// <summary>
/// Extract data from the overlay
Expand Down