mirror of
https://github.com/SabreTools/SabreTools.Serialization.git
synced 2026-07-09 02:16:55 +00:00
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:
committed by
GitHub
parent
ed9ecf0da9
commit
d5ab37a5a6
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user