diff --git a/.github/workflows/check_pr.yml b/.github/workflows/check_pr.yml
index c9e23ecd..cccf5f5d 100644
--- a/.github/workflows/check_pr.yml
+++ b/.github/workflows/check_pr.yml
@@ -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
diff --git a/SabreTools.Serialization/Models/InstallShieldExecutable/ExtractableFile.cs b/SabreTools.Serialization/Models/InstallShieldExecutable/ExtractableFile.cs
new file mode 100644
index 00000000..10d54bcc
--- /dev/null
+++ b/SabreTools.Serialization/Models/InstallShieldExecutable/ExtractableFile.cs
@@ -0,0 +1,25 @@
+namespace SabreTools.Data.Models.InstallShieldExecutable
+{
+ public class ExtractableFile
+ {
+ ///
+ /// Name of the file, only ASCII characters(?)
+ ///
+ public string? Name { get; set; }
+
+ ///
+ /// Path of the file, only ASCII characters(?), seems to usually use \ filepaths
+ ///
+ public string? Path { get; set; }
+
+ ///
+ /// Version of the file
+ ///
+ public string? Version { get; set; }
+
+ ///
+ /// Length of the file. Stored in the installshield executable as a string.
+ ///
+ public ulong Length { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/SabreTools.Serialization/Readers/InstallShieldExecutableFile.cs b/SabreTools.Serialization/Readers/InstallShieldExecutableFile.cs
new file mode 100644
index 00000000..56305e2f
--- /dev/null
+++ b/SabreTools.Serialization/Readers/InstallShieldExecutableFile.cs
@@ -0,0 +1,66 @@
+using System.IO;
+using SabreTools.Data.Models.InstallShieldExecutable;
+using SabreTools.IO.Extensions;
+
+namespace SabreTools.Serialization.Readers
+{
+ public class InstallShieldExecutableFile : BaseBinaryReader
+ {
+ 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;
+ }
+ }
+
+ ///
+ /// Parse a Stream into an InstallShield Executable file header
+ ///
+ /// Stream to parse
+ /// Filled InstallShield Executable file header on success, null on error
+ 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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/SabreTools.Serialization/Wrappers/PortableExecutable.Extraction.cs b/SabreTools.Serialization/Wrappers/PortableExecutable.Extraction.cs
index fb167dd7..b76a0c49 100644
--- a/SabreTools.Serialization/Wrappers/PortableExecutable.Extraction.cs
+++ b/SabreTools.Serialization/Wrappers/PortableExecutable.Extraction.cs
@@ -18,6 +18,7 @@ public partial class PortableExecutable : IExtractable
/// - SFX archives
/// + 7z
/// + Advanced Installer
+ /// + InstallShield Executables
/// + MS-CAB
/// + PKZIP
/// + RAR
@@ -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);
@@ -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;
}
@@ -158,6 +160,85 @@ public bool ExtractCExe(string outputDirectory, bool includeDebug)
return false;
}
}
+
+ ///
+ /// Extract data from an InstallShield Executable
+ ///
+ /// Output directory to write to
+ /// True to include debug data, false otherwise
+ /// True if extraction succeeded, false otherwise
+ 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;
+ }
+ }
///
/// Extract data from the overlay