Add extraction for installshield executables. (#27)

* Add extractor for installshield executables.

* Add comment

* Fix intendation.

* Ensure PRs recursively pull

* Rebase

* Fix formatting for parsex

* newline before obj.name

* Changed while loop check to a position vs length check

* Specify type for chunkSize

* declare chunksize as a const int outside of the loop

* Open file writer with using

* Call fs.flush after every write.

* Improved chunk writing loop.

* Remove now-unecessary variable

* Properly handle extracting files of size greater than int32.

* Use long for overlay address

* create deserializer outside of loop.

* Move null check.

* Don't cache position.

---------

Co-authored-by: Matt Nadareski <mnadareski@outlook.com>
This commit is contained in:
HeroponRikiBestest
2025-10-23 15:38:51 -04:00
committed by GitHub
parent ed9ecf0da9
commit d5ab37a5a6
3 changed files with 174 additions and 2 deletions

View File

@@ -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; }
}
}

View File

@@ -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>
{
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;
}
}
}

View File

@@ -18,6 +18,7 @@ namespace SabreTools.Serialization.Wrappers
/// - SFX archives
/// + 7z
/// + Advanced Installer
/// + InstallShield Executables
/// + MS-CAB
/// + PKZIP
/// + RAR
@@ -28,6 +29,7 @@ namespace SabreTools.Serialization.Wrappers
{
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);
@@ -37,10 +39,10 @@ namespace SabreTools.Serialization.Wrappers
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;
}
@@ -158,6 +160,85 @@ namespace SabreTools.Serialization.Wrappers
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