mirror of
https://github.com/SabreTools/SabreTools.Serialization.git
synced 2026-09-22 14:55:11 +00:00
Minor cleanup and additions
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using SabreTools.Serialization.Readers;
|
||||
using Xunit;
|
||||
|
||||
namespace SabreTools.Serialization.Test.Readers
|
||||
{
|
||||
public class InstallShieldExecutableFileTests
|
||||
{
|
||||
[Fact]
|
||||
public void NullArray_Null()
|
||||
{
|
||||
byte[]? data = null;
|
||||
int offset = 0;
|
||||
var deserializer = new InstallShieldExecutableFile();
|
||||
|
||||
var actual = deserializer.Deserialize(data, offset);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptyArray_Null()
|
||||
{
|
||||
byte[]? data = [];
|
||||
int offset = 0;
|
||||
var deserializer = new InstallShieldExecutableFile();
|
||||
|
||||
var actual = deserializer.Deserialize(data, offset);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidArray_Null()
|
||||
{
|
||||
byte[]? data = [.. Enumerable.Repeat<byte>(0xFF, 1024)];
|
||||
int offset = 0;
|
||||
var deserializer = new InstallShieldExecutableFile();
|
||||
|
||||
var actual = deserializer.Deserialize(data, offset);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NullStream_Null()
|
||||
{
|
||||
Stream? data = null;
|
||||
var deserializer = new InstallShieldExecutableFile();
|
||||
|
||||
var actual = deserializer.Deserialize(data);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptyStream_Null()
|
||||
{
|
||||
Stream? data = new MemoryStream([]);
|
||||
var deserializer = new InstallShieldExecutableFile();
|
||||
|
||||
var actual = deserializer.Deserialize(data);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidStream_Null()
|
||||
{
|
||||
Stream? data = new MemoryStream([.. Enumerable.Repeat<byte>(0xFF, 1024)]);
|
||||
var deserializer = new InstallShieldExecutableFile();
|
||||
|
||||
var actual = deserializer.Deserialize(data);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,23 @@
|
||||
namespace SabreTools.Data.Models.InstallShieldExecutable
|
||||
{
|
||||
public class ExtractableFile
|
||||
public class FileEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the file, only ASCII characters(?)
|
||||
/// Name of the file
|
||||
/// </summary>
|
||||
/// <remarks>May only contain ASCII (7-bit) characters</remarks>
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Path of the file, only ASCII characters(?), seems to usually use \ filepaths
|
||||
/// Path of the file, seems to usually use \ filepaths
|
||||
/// </summary>
|
||||
public string? Path { get; set; }
|
||||
/// <remarks>May only contain ASCII (7-bit) characters</remarks>
|
||||
public string? Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Version of the file
|
||||
/// </summary>
|
||||
/// <remarks>May only contain ASCII (7-bit) characters</remarks>
|
||||
public string? Version { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -22,4 +25,4 @@ namespace SabreTools.Data.Models.InstallShieldExecutable
|
||||
/// </summary>
|
||||
public ulong Length { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
namespace SabreTools.Data.Models.InstallShieldExecutable
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the layout of of the overlay area of an
|
||||
/// InstallShield executable.
|
||||
///
|
||||
/// The layout of this is derived from the layout in the
|
||||
/// physical file.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// According to ISx source, there are two categories of installshield executables,
|
||||
/// plain and encrypted. Encrypted has two different "types" it can be, specified by
|
||||
/// a header. "InstallShield" and a newer format from 2015?-onwards called "ISSetupStream".
|
||||
/// Plain executables have no central header, and each file is unencrypted. Files
|
||||
/// in "InstallShield" encrypted executables have encryption applied over block sizes
|
||||
/// of 1024 bytes, and files in "ISSetupStream" encrypted executables are encrypted
|
||||
/// per-file. There's also something about leading data that isn't explained
|
||||
/// (at least not clearly), and these encrypted executables can also additionally have
|
||||
/// their files compressed with inflate.
|
||||
///
|
||||
/// While not stated in ISx; from experience, executables with "InstallShield" often
|
||||
/// (if not always?) mainly consist of a singular, large MSI installer along with some
|
||||
/// helper files, whereas plain executables often (if not always?) mainly consist of
|
||||
/// regular installshield cabinets within. At the moment, this code only supports and
|
||||
/// documents the plain variant. Clearer naming and separation between the types is yet
|
||||
/// to come.
|
||||
/// </remarks>
|
||||
/// TODO: Look into making the array a dictionary
|
||||
/// There is no unified header or footer that indicates a file
|
||||
/// table, so having either each file entry cache the data
|
||||
/// or be associated with an offset may make it more useful.
|
||||
///
|
||||
/// It will need to be made very apparent that the dictionary
|
||||
/// does not directly represent the structure.
|
||||
public class SFX
|
||||
{
|
||||
/// <summary>
|
||||
/// Set of file entries
|
||||
/// </summary>
|
||||
public FileEntry[]? Entries { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,10 @@ using SabreTools.IO.Extensions;
|
||||
|
||||
namespace SabreTools.Serialization.Readers
|
||||
{
|
||||
public class InstallShieldExecutableFile : BaseBinaryReader<ExtractableFile>
|
||||
// TODO: This should parse an entire SFX, not just a single entry
|
||||
public class InstallShieldExecutableFile : BaseBinaryReader<FileEntry>
|
||||
{
|
||||
public override ExtractableFile? Deserialize(Stream? data)
|
||||
public override FileEntry? Deserialize(Stream? data)
|
||||
{
|
||||
// If the data is invalid
|
||||
if (data == null || !data.CanRead)
|
||||
@@ -17,50 +18,48 @@ namespace SabreTools.Serialization.Readers
|
||||
// Cache the initial offset
|
||||
long initialOffset = data.Position;
|
||||
|
||||
// Try to parse the header
|
||||
var header = ParseExtractableFileHeader(data);
|
||||
if (header == null)
|
||||
// Try to parse the entry
|
||||
var fileEntry = ParseFileEntry(data);
|
||||
if (fileEntry == null)
|
||||
return null;
|
||||
|
||||
return header;
|
||||
return fileEntry;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore the actual error
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an InstallShield Executable file header
|
||||
/// Parse a Stream into a FileEntry
|
||||
/// </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)
|
||||
/// <returns>Filled FileEntry on success, null on error</returns>
|
||||
public static FileEntry? ParseFileEntry(Stream? data)
|
||||
{
|
||||
var obj = new ExtractableFile();
|
||||
|
||||
var obj = new FileEntry();
|
||||
|
||||
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))
|
||||
|
||||
var lengthString = data.ReadNullTerminatedAnsiString();
|
||||
if (lengthString == null || !ulong.TryParse(lengthString, out var lengthValue))
|
||||
return null;
|
||||
|
||||
obj.Length = foundVersion;
|
||||
if (obj.Name == null)
|
||||
return null;
|
||||
|
||||
|
||||
obj.Length = lengthValue;
|
||||
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,8 +42,8 @@ namespace SabreTools.Serialization.Wrappers
|
||||
bool overlay = cai || issexe || spoon || wiseOverlay
|
||||
|| ExtractFromOverlay(outputDirectory, includeDebug);
|
||||
|
||||
return cai || cexe || issexe || matroschka || overlay || resources || spoon
|
||||
|| wiseOverlay || wiseSection;
|
||||
return cai || cexe || issexe || matroschka || overlay || resources
|
||||
|| spoon || wiseOverlay || wiseSection;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -160,7 +160,7 @@ namespace SabreTools.Serialization.Wrappers
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Extract data from an InstallShield Executable
|
||||
/// </summary>
|
||||
@@ -171,37 +171,32 @@ namespace SabreTools.Serialization.Wrappers
|
||||
{
|
||||
try
|
||||
{
|
||||
long overlayAddress = OverlayAddress;
|
||||
|
||||
// Return if overlay doesn't exist.
|
||||
if (overlayAddress == -1)
|
||||
long overlayAddress = OverlayAddress;
|
||||
if (overlayAddress < 0)
|
||||
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)
|
||||
const int chunkSize = 64 * 1024;
|
||||
var reader = new Readers.InstallShieldExecutableFile();
|
||||
|
||||
lock (_dataSourceLock)
|
||||
{
|
||||
lock (_dataSourceLock)
|
||||
// Ensure the stream is starting at the overlay address
|
||||
_dataSource.Seek(overlayAddress, SeekOrigin.Begin);
|
||||
|
||||
while (_dataSource.Position < _dataSource.Length)
|
||||
{
|
||||
// Try to deserialize the source data
|
||||
|
||||
var entry = deserializer.Deserialize(_dataSource);
|
||||
var entry = reader.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)
|
||||
long length = (long)entry.Length;
|
||||
if (length > _dataSource.Length - _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('/', '\\');
|
||||
@@ -216,21 +211,19 @@ namespace SabreTools.Serialization.Wrappers
|
||||
|
||||
// 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);
|
||||
int bytesToRead = (int)Math.Min(length, chunkSize);
|
||||
|
||||
byte[] buffer = _dataSource.ReadBytes(bytesToRead);
|
||||
fs.Write(buffer, 0, bytesToRead);
|
||||
fs.Flush();
|
||||
|
||||
length -= bytesToRead;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
Reference in New Issue
Block a user