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