mirror of
https://github.com/SabreTools/SabreTools.Serialization.git
synced 2026-04-05 22:01:33 +00:00
This change looks dramatic, but it's just separating out the already-split namespaces into separate top-level folders. In theory, every single one could be built into their own Nuget package. `SabreTools.Serialization` still builds the normal Nuget package that is used by all other projects and includes all namespaces.
836 lines
30 KiB
C#
836 lines
30 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Text.RegularExpressions;
|
|
using SabreTools.Data.Models.InstallShieldCabinet;
|
|
using SabreTools.Hashing;
|
|
using SabreTools.IO.Compression.zlib;
|
|
using SabreTools.IO.Extensions;
|
|
using static SabreTools.Data.Models.InstallShieldCabinet.Constants;
|
|
|
|
#pragma warning disable SYSLIB1045 // Convert to 'GeneratedRegexAttribute'
|
|
namespace SabreTools.Wrappers
|
|
{
|
|
public partial class InstallShieldCabinet : IExtractable
|
|
{
|
|
#region Extension Properties
|
|
|
|
/// <summary>
|
|
/// Reference to the next cabinet header
|
|
/// </summary>
|
|
/// <remarks>Only used in multi-file</remarks>
|
|
public InstallShieldCabinet? Next { get; set; }
|
|
|
|
/// <summary>
|
|
/// Reference to the next previous header
|
|
/// </summary>
|
|
/// <remarks>Only used in multi-file</remarks>
|
|
public InstallShieldCabinet? Prev { get; set; }
|
|
|
|
/// <summary>
|
|
/// Volume index ID, 0 for headers
|
|
/// </summary>
|
|
/// <remarks>Only used in multi-file</remarks>
|
|
public ushort VolumeID { get; set; }
|
|
|
|
#endregion
|
|
|
|
#region Extraction State
|
|
|
|
/// <summary>
|
|
/// Base filename path for related CAB files
|
|
/// </summary>
|
|
internal string? FilenamePattern { get; set; }
|
|
|
|
#endregion
|
|
|
|
#region Constants
|
|
|
|
/// <summary>
|
|
/// Default buffer size
|
|
/// </summary>
|
|
private const int BUFFER_SIZE = 64 * 1024;
|
|
|
|
/// <summary>
|
|
/// Maximum size of the window in bits
|
|
/// </summary>
|
|
private const int MAX_WBITS = 15;
|
|
|
|
#endregion
|
|
|
|
#region Cabinet Set
|
|
|
|
/// <summary>
|
|
/// Open a cabinet set for reading, if possible
|
|
/// </summary>
|
|
/// <param name="pattern">Filename pattern for matching cabinet files</param>
|
|
/// <returns>Wrapper representing the set, null on error</returns>
|
|
public static InstallShieldCabinet? OpenSet(string? pattern)
|
|
{
|
|
// An invalid pattern means no cabinet files
|
|
if (string.IsNullOrEmpty(pattern))
|
|
return null;
|
|
|
|
// Create a placeholder wrapper for output
|
|
InstallShieldCabinet? set = null;
|
|
|
|
// Loop until there are no parts left
|
|
bool iterate = true;
|
|
InstallShieldCabinet? previous = null;
|
|
for (ushort i = 1; iterate; i++)
|
|
{
|
|
var file = OpenFileForReading(pattern, i, HEADER_SUFFIX);
|
|
if (file is not null)
|
|
iterate = false;
|
|
else
|
|
file = OpenFileForReading(pattern, i, CABINET_SUFFIX);
|
|
|
|
if (file is null)
|
|
break;
|
|
|
|
var current = Create(file);
|
|
if (current is null)
|
|
break;
|
|
|
|
current.VolumeID = i;
|
|
if (previous is not null)
|
|
{
|
|
previous.Next = current;
|
|
current.Prev = previous;
|
|
}
|
|
else
|
|
{
|
|
set = current;
|
|
previous = current;
|
|
}
|
|
}
|
|
|
|
// Set the pattern, if possible
|
|
set?.FilenamePattern = pattern;
|
|
|
|
return set;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Open the numbered cabinet set volume
|
|
/// </summary>
|
|
/// <param name="volumeId">Volume ID, 1-indexed</param>
|
|
/// <returns>Wrapper representing the volume on success, null otherwise</returns>
|
|
public InstallShieldCabinet? OpenVolume(ushort volumeId, out Stream? volumeStream)
|
|
{
|
|
// Normalize the volume ID for odd cases
|
|
if (volumeId == ushort.MinValue || volumeId == ushort.MaxValue)
|
|
volumeId = 1;
|
|
|
|
// Try to open the file as a stream
|
|
volumeStream = OpenFileForReading(FilenamePattern, volumeId, CABINET_SUFFIX);
|
|
if (volumeStream is null)
|
|
{
|
|
Console.Error.WriteLine($"Failed to open input cabinet file {volumeId}");
|
|
return null;
|
|
}
|
|
|
|
// Try to parse the stream into a cabinet
|
|
var volume = Create(volumeStream);
|
|
if (volume is null)
|
|
{
|
|
Console.Error.WriteLine($"Failed to open input cabinet file {volumeId}");
|
|
return null;
|
|
}
|
|
|
|
// Set the volume ID and return
|
|
volume.VolumeID = volumeId;
|
|
return volume;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Open a cabinet file for reading
|
|
/// </summary>
|
|
/// <param name="index">Cabinet part index to be opened</param>
|
|
/// <param name="suffix">Cabinet files suffix (e.g. `.cab`)</param>
|
|
/// <returns>A Stream representing the cabinet part, null on error</returns>
|
|
public Stream? OpenFileForReading(int index, string suffix)
|
|
=> OpenFileForReading(FilenamePattern, index, suffix);
|
|
|
|
/// <summary>
|
|
/// Create the generic filename pattern to look for from the input filename
|
|
/// </summary>
|
|
/// <returns>String representing the filename pattern for a cabinet set, null on error</returns>
|
|
private static string? CreateFilenamePattern(string filename)
|
|
{
|
|
string? pattern = null;
|
|
if (string.IsNullOrEmpty(filename))
|
|
return pattern;
|
|
|
|
string? directory = Path.GetDirectoryName(Path.GetFullPath(filename));
|
|
if (directory is not null)
|
|
pattern = Path.Combine(directory, Path.GetFileNameWithoutExtension(filename));
|
|
else
|
|
pattern = Path.GetFileNameWithoutExtension(filename);
|
|
|
|
return new Regex(@"\d+$").Replace(pattern, string.Empty);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Open a cabinet file for reading
|
|
/// </summary>
|
|
/// <param name="pattern">Filename pattern for matching cabinet files</param>
|
|
/// <param name="index">Cabinet part index to be opened</param>
|
|
/// <param name="suffix">Cabinet files suffix (e.g. `.cab`)</param>
|
|
/// <returns>A Stream representing the cabinet part, null on error</returns>
|
|
private static FileStream? OpenFileForReading(string? pattern, int index, string suffix)
|
|
{
|
|
// An invalid pattern means no cabinet files
|
|
if (string.IsNullOrEmpty(pattern))
|
|
return null;
|
|
|
|
// Attempt lower-case extension
|
|
string filename = $"{pattern}{index}.{suffix}";
|
|
if (File.Exists(filename))
|
|
return File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
|
|
|
// Attempt upper-case extension
|
|
filename = $"{pattern}{index}.{suffix.ToUpperInvariant()}";
|
|
if (File.Exists(filename))
|
|
return File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
|
|
|
return null;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Extraction
|
|
|
|
/// <inheritdoc/>
|
|
public bool Extract(string outputDirectory, bool includeDebug)
|
|
{
|
|
// Open the full set if possible
|
|
var cabinet = this;
|
|
if (Filename is not null)
|
|
{
|
|
// Get the name of the first cabinet file or header
|
|
string pattern = CreateFilenamePattern(Filename)!;
|
|
bool cabinetHeaderExists = File.Exists(pattern + "1.hdr");
|
|
bool shouldScanCabinet = cabinetHeaderExists
|
|
? Filename.Equals(pattern + "1.hdr", StringComparison.OrdinalIgnoreCase)
|
|
: Filename.Equals(pattern + "1.cab", StringComparison.OrdinalIgnoreCase);
|
|
|
|
// If we have anything but the first file
|
|
if (!shouldScanCabinet)
|
|
return false;
|
|
|
|
// Open the set from the pattern
|
|
cabinet = OpenSet(pattern);
|
|
}
|
|
|
|
// If the cabinet set could not be opened
|
|
if (cabinet is null)
|
|
return false;
|
|
|
|
try
|
|
{
|
|
for (int i = 0; i < cabinet.FileCount; i++)
|
|
{
|
|
try
|
|
{
|
|
// Check if the file is valid first
|
|
if (!cabinet.FileIsValid(i))
|
|
continue;
|
|
|
|
// Ensure directory separators are consistent
|
|
string filename = cabinet.GetFileName(i) ?? $"BAD_FILENAME{i}";
|
|
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 is not null && !Directory.Exists(directoryName))
|
|
Directory.CreateDirectory(directoryName);
|
|
|
|
cabinet.FileSave(i, filename, includeDebug);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (includeDebug) Console.Error.WriteLine(ex);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (includeDebug) Console.Error.WriteLine(ex);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Save the file at the given index to the filename specified
|
|
/// </summary>
|
|
public bool FileSave(int index, string filename, bool includeDebug, bool useOld = false)
|
|
{
|
|
// Get the file descriptor
|
|
if (!TryGetFileDescriptor(index, out var fileDescriptor) || fileDescriptor is null)
|
|
return false;
|
|
|
|
// If the file is split
|
|
if (fileDescriptor.LinkFlags == LinkFlags.LINK_PREV)
|
|
return FileSave((int)fileDescriptor.LinkPrevious, filename, includeDebug, useOld);
|
|
|
|
// Get the reader at the index
|
|
var reader = Reader.Create(this, index, fileDescriptor);
|
|
if (reader is null)
|
|
return false;
|
|
|
|
// Create the output file and hasher
|
|
using var fs = File.Open(filename, FileMode.Create, FileAccess.Write, FileShare.None);
|
|
var md5 = new HashWrapper(HashType.MD5);
|
|
|
|
ulong readBytesLeft = GetReadableBytes(fileDescriptor);
|
|
ulong writeBytesLeft = GetWritableBytes(fileDescriptor);
|
|
byte[] outputBuffer = new byte[BUFFER_SIZE];
|
|
ulong totalWritten = 0;
|
|
|
|
// Cache the expected values
|
|
ulong storedSize = readBytesLeft;
|
|
|
|
// Read while there are bytes remaining
|
|
while (readBytesLeft > 0 && readBytesLeft <= storedSize)
|
|
{
|
|
uint bytesToWrite = BUFFER_SIZE;
|
|
int result;
|
|
|
|
// Handle compressed files
|
|
#if NET20 || NET35
|
|
if ((fileDescriptor.Flags & FileFlags.FILE_COMPRESSED) != 0)
|
|
#else
|
|
if (fileDescriptor.Flags.HasFlag(FileFlags.FILE_COMPRESSED))
|
|
#endif
|
|
{
|
|
// Attempt to read the length value
|
|
byte[] lengthArr = new byte[sizeof(ushort)];
|
|
if (!reader.Read(lengthArr, 0, lengthArr.Length))
|
|
{
|
|
Console.Error.WriteLine($"Failed to read {lengthArr.Length} bytes of file {index} ({GetFileName(index)}) from input cabinet file {fileDescriptor.Volume}");
|
|
reader.Dispose();
|
|
fs?.Close();
|
|
return false;
|
|
}
|
|
|
|
// Attempt to read the specified number of bytes
|
|
uint bytesToRead = BitConverter.ToUInt16(lengthArr, 0);
|
|
byte[] inputBuffer = new byte[BUFFER_SIZE];
|
|
if (!reader.Read(inputBuffer, 0, bytesToRead))
|
|
{
|
|
Console.Error.WriteLine($"Failed to read {lengthArr.Length} bytes of file {index} ({GetFileName(index)}) from input cabinet file {fileDescriptor.Volume}");
|
|
reader.Dispose();
|
|
fs?.Close();
|
|
return false;
|
|
}
|
|
|
|
// Uncompress into a buffer
|
|
if (useOld)
|
|
result = UncompressOld(outputBuffer, ref bytesToWrite, inputBuffer, ref bytesToRead);
|
|
else
|
|
result = Uncompress(outputBuffer, ref bytesToWrite, inputBuffer, ref bytesToRead);
|
|
|
|
// If we didn't get a positive result that's not a data error (false positives)
|
|
if (result != zlibConst.Z_OK && result != zlibConst.Z_DATA_ERROR)
|
|
{
|
|
Console.Error.WriteLine($"Decompression failed with code {result.ToZlibConstName()}. bytes_to_read={bytesToRead}, volume={fileDescriptor.Volume}, read_bytes={bytesToRead}");
|
|
reader.Dispose();
|
|
fs?.Close();
|
|
return false;
|
|
}
|
|
|
|
// Set remaining bytes
|
|
readBytesLeft -= 2;
|
|
readBytesLeft -= bytesToRead;
|
|
}
|
|
|
|
// Handle uncompressed files
|
|
else
|
|
{
|
|
bytesToWrite = (uint)Math.Min(readBytesLeft, BUFFER_SIZE);
|
|
if (!reader.Read(outputBuffer, 0, (int)bytesToWrite))
|
|
{
|
|
Console.Error.WriteLine($"Failed to write {bytesToWrite} bytes from input cabinet file {fileDescriptor.Volume}");
|
|
reader.Dispose();
|
|
fs?.Close();
|
|
return false;
|
|
}
|
|
|
|
// Set remaining bytes
|
|
readBytesLeft -= bytesToWrite;
|
|
}
|
|
|
|
// Hash and write the next block
|
|
bytesToWrite = (uint)Math.Min(bytesToWrite, writeBytesLeft);
|
|
md5.Process(outputBuffer, 0, (int)bytesToWrite);
|
|
fs?.Write(outputBuffer, 0, (int)bytesToWrite);
|
|
|
|
totalWritten += bytesToWrite;
|
|
writeBytesLeft -= bytesToWrite;
|
|
}
|
|
|
|
// Validate the number of bytes written
|
|
if (fileDescriptor.ExpandedSize != totalWritten)
|
|
if (includeDebug) Console.WriteLine($"Expanded size of file {index} ({GetFileName(index)}) expected to be {fileDescriptor.ExpandedSize}, but was {totalWritten}");
|
|
|
|
// Finalize output values
|
|
md5.Terminate();
|
|
reader?.Dispose();
|
|
fs?.Close();
|
|
|
|
// Validate the data written, if required
|
|
if (MajorVersion >= 6)
|
|
{
|
|
string expectedMd5 = BitConverter.ToString(fileDescriptor.MD5);
|
|
expectedMd5 = expectedMd5.ToLowerInvariant().Replace("-", string.Empty);
|
|
|
|
string? actualMd5 = md5.CurrentHashString;
|
|
if (actualMd5 is null || actualMd5 != expectedMd5)
|
|
{
|
|
Console.Error.WriteLine($"MD5 checksum failure for file {index} ({GetFileName(index)})");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Save the file at the given index to the filename specified as raw
|
|
/// </summary>
|
|
public bool FileSaveRaw(int index, string filename)
|
|
{
|
|
// Get the file descriptor
|
|
if (!TryGetFileDescriptor(index, out var fileDescriptor) || fileDescriptor is null)
|
|
return false;
|
|
|
|
// If the file is split
|
|
if (fileDescriptor.LinkFlags == LinkFlags.LINK_PREV)
|
|
return FileSaveRaw((int)fileDescriptor.LinkPrevious, filename);
|
|
|
|
// Get the reader at the index
|
|
var reader = Reader.Create(this, index, fileDescriptor);
|
|
if (reader is null)
|
|
return false;
|
|
|
|
// Create the output file
|
|
using var fs = File.Open(filename, FileMode.Create, FileAccess.Write, FileShare.None);
|
|
|
|
ulong bytesLeft = GetReadableBytes(fileDescriptor);
|
|
byte[] outputBuffer = new byte[BUFFER_SIZE];
|
|
|
|
// Read while there are bytes remaining
|
|
while (bytesLeft > 0)
|
|
{
|
|
ulong bytesToWrite = Math.Min(bytesLeft, BUFFER_SIZE);
|
|
if (!reader.Read(outputBuffer, 0, (int)bytesToWrite))
|
|
{
|
|
Console.Error.WriteLine($"Failed to read {bytesToWrite} bytes from input cabinet file {fileDescriptor.Volume}");
|
|
reader.Dispose();
|
|
fs?.Close();
|
|
return false;
|
|
}
|
|
|
|
// Set remaining bytes
|
|
bytesLeft -= (uint)bytesToWrite;
|
|
|
|
// Write the next block
|
|
fs.Write(outputBuffer, 0, (int)bytesToWrite);
|
|
}
|
|
|
|
// Finalize output values
|
|
reader.Dispose();
|
|
fs?.Close();
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Uncompress a source byte array to a destination
|
|
/// </summary>
|
|
private static unsafe int Uncompress(byte[] dest, ref uint destLen, byte[] source, ref uint sourceLen)
|
|
{
|
|
fixed (byte* sourcePtr = source, destPtr = dest)
|
|
{
|
|
var stream = new ZLib.z_stream_s
|
|
{
|
|
next_in = sourcePtr,
|
|
avail_in = sourceLen,
|
|
next_out = destPtr,
|
|
avail_out = destLen,
|
|
};
|
|
|
|
// make second parameter negative to disable checksum verification
|
|
int err = ZLib.inflateInit2_(stream, -MAX_WBITS, ZLib.zlibVersion(), source.Length);
|
|
if (err != zlibConst.Z_OK)
|
|
return err;
|
|
|
|
err = ZLib.inflate(stream, zlibConst.Z_FINISH);
|
|
if (err != zlibConst.Z_OK && err != zlibConst.Z_STREAM_END)
|
|
{
|
|
ZLib.inflateEnd(stream);
|
|
return err;
|
|
}
|
|
|
|
destLen = stream.total_out;
|
|
sourceLen = stream.total_in;
|
|
return ZLib.inflateEnd(stream);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Uncompress a source byte array to a destination (old version)
|
|
/// </summary>
|
|
private static unsafe int UncompressOld(byte[] dest, ref uint destLen, byte[] source, ref uint sourceLen)
|
|
{
|
|
fixed (byte* sourcePtr = source, destPtr = dest)
|
|
{
|
|
var stream = new ZLib.z_stream_s
|
|
{
|
|
next_in = sourcePtr,
|
|
avail_in = sourceLen,
|
|
next_out = destPtr,
|
|
avail_out = destLen,
|
|
};
|
|
|
|
destLen = 0;
|
|
sourceLen = 0;
|
|
|
|
// make second parameter negative to disable checksum verification
|
|
int err = ZLib.inflateInit2_(stream, -MAX_WBITS, ZLib.zlibVersion(), source.Length);
|
|
if (err != zlibConst.Z_OK)
|
|
return err;
|
|
|
|
while (stream.avail_in > 1)
|
|
{
|
|
err = ZLib.inflate(stream, zlibConst.Z_BLOCK);
|
|
if (err != zlibConst.Z_OK)
|
|
{
|
|
ZLib.inflateEnd(stream);
|
|
return err;
|
|
}
|
|
}
|
|
|
|
destLen = stream.total_out;
|
|
sourceLen = stream.total_in;
|
|
return ZLib.inflateEnd(stream);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Obfuscation
|
|
|
|
/// <summary>
|
|
/// Deobfuscate a buffer
|
|
/// </summary>
|
|
public static void Deobfuscate(byte[] buffer, long size, ref uint offset)
|
|
{
|
|
offset = Deobfuscate(buffer, size, offset);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deobfuscate a buffer with a seed value
|
|
/// </summary>
|
|
/// <remarks>Seed is 0 at file start</remarks>
|
|
public static uint Deobfuscate(byte[] buffer, long size, uint seed)
|
|
{
|
|
for (int i = 0; size > 0; size--, i++, seed++)
|
|
{
|
|
buffer[i] = (byte)(ROR8(buffer[i] ^ 0xd5, 2) - (seed % 0x47));
|
|
}
|
|
|
|
return seed;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Obfuscate a buffer
|
|
/// </summary>
|
|
public static void Obfuscate(byte[] buffer, long size, ref uint offset)
|
|
{
|
|
offset = Obfuscate(buffer, size, offset);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Obfuscate a buffer with a seed value
|
|
/// </summary>
|
|
/// <remarks>Seed is 0 at file start</remarks>
|
|
public static uint Obfuscate(byte[] buffer, long size, uint seed)
|
|
{
|
|
for (int i = 0; size > 0; size--, i++, seed++)
|
|
{
|
|
buffer[i] = (byte)(ROL8(buffer[i] ^ 0xd5, 2) + (seed % 0x47));
|
|
}
|
|
|
|
return seed;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Rotate Right 8
|
|
/// </summary>
|
|
private static int ROR8(int x, byte n) => (x >> n) | (x << (8 - n));
|
|
|
|
/// <summary>
|
|
/// Rotate Left 8
|
|
/// </summary>
|
|
private static int ROL8(int x, byte n) => (x << n) | (x >> (8 - n));
|
|
|
|
#endregion
|
|
|
|
#region Helper Classes
|
|
|
|
/// <summary>
|
|
/// Helper to read a single file from a cabinet set
|
|
/// </summary>
|
|
private class Reader : IDisposable
|
|
{
|
|
#region Private Instance Variables
|
|
|
|
/// <summary>
|
|
/// Cabinet file to read from
|
|
/// </summary>
|
|
private readonly InstallShieldCabinet _cabinet;
|
|
|
|
/// <summary>
|
|
/// Currently selected index
|
|
/// </summary>
|
|
private readonly uint _index;
|
|
|
|
/// <summary>
|
|
/// File descriptor defining the currently selected index
|
|
/// </summary>
|
|
private readonly FileDescriptor _fileDescriptor;
|
|
|
|
/// <summary>
|
|
/// Offset in the data where the file exists
|
|
/// </summary>
|
|
private ulong _dataOffset;
|
|
|
|
/// <summary>
|
|
/// Number of bytes left in the current volume
|
|
/// </summary>
|
|
private ulong _volumeBytesLeft;
|
|
|
|
/// <summary>
|
|
/// Handle to the current volume stream
|
|
/// </summary>
|
|
private Stream? _volumeFile;
|
|
|
|
/// <summary>
|
|
/// Current volume header
|
|
/// </summary>
|
|
private VolumeHeader? _volumeHeader;
|
|
|
|
/// <summary>
|
|
/// Current volume ID
|
|
/// </summary>
|
|
private ushort _volumeId;
|
|
|
|
/// <summary>
|
|
/// Offset for obfuscation seed
|
|
/// </summary>
|
|
private uint _obfuscationOffset;
|
|
|
|
#endregion
|
|
|
|
#region Constructors
|
|
|
|
private Reader(InstallShieldCabinet cabinet, uint index, FileDescriptor fileDescriptor)
|
|
{
|
|
_cabinet = cabinet;
|
|
_index = index;
|
|
_fileDescriptor = fileDescriptor;
|
|
}
|
|
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// Create a new <see cref="Reader"> from an existing cabinet, index, and file descriptor
|
|
/// </summary>
|
|
public static Reader? Create(InstallShieldCabinet cabinet, int index, FileDescriptor fileDescriptor)
|
|
{
|
|
var reader = new Reader(cabinet, (uint)index, fileDescriptor);
|
|
for (; ; )
|
|
{
|
|
// If the volume is invalid
|
|
if (!reader.OpenVolume(fileDescriptor.Volume))
|
|
{
|
|
Console.Error.WriteLine($"Failed to open volume {fileDescriptor.Volume}");
|
|
return null;
|
|
}
|
|
else if (reader._volumeFile is null || reader._volumeHeader is null)
|
|
{
|
|
Console.Error.WriteLine($"Volume {fileDescriptor.Volume} is invalid");
|
|
return null;
|
|
}
|
|
|
|
// Start with the correct volume for IS5 cabinets
|
|
if (reader._cabinet.MajorVersion <= 5 && index > (int)reader._volumeHeader.LastFileIndex)
|
|
{
|
|
// Normalize the volume ID for odd cases
|
|
if (fileDescriptor.Volume == ushort.MinValue || fileDescriptor.Volume == ushort.MaxValue)
|
|
fileDescriptor.Volume = 1;
|
|
|
|
fileDescriptor.Volume++;
|
|
continue;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
return reader;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Dispose of the current object
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
_volumeFile?.Close();
|
|
}
|
|
|
|
#region Reading
|
|
|
|
/// <summary>
|
|
/// Read a certain number of bytes from the current volume
|
|
/// </summary>
|
|
public bool Read(byte[] buffer, int start, long size)
|
|
{
|
|
long bytesLeft = size;
|
|
while (bytesLeft > 0)
|
|
{
|
|
// Open the next volume, if necessary
|
|
if (_volumeBytesLeft == 0)
|
|
{
|
|
if (!OpenNextVolume(out _))
|
|
return false;
|
|
}
|
|
|
|
// Get the number of bytes to read from this volume
|
|
int bytesToRead = (int)Math.Min(bytesLeft, (long)_volumeBytesLeft);
|
|
if (bytesToRead == 0)
|
|
break;
|
|
|
|
// Read as much as possible from this volume
|
|
if (bytesToRead != _volumeFile!.Read(buffer, start, bytesToRead))
|
|
return false;
|
|
|
|
// Set the number of bytes left
|
|
start += bytesToRead;
|
|
bytesLeft -= bytesToRead;
|
|
_volumeBytesLeft -= (uint)bytesToRead;
|
|
}
|
|
|
|
#if NET20 || NET35
|
|
if ((_fileDescriptor.Flags & FileFlags.FILE_OBFUSCATED) != 0)
|
|
#else
|
|
if (_fileDescriptor.Flags.HasFlag(FileFlags.FILE_OBFUSCATED))
|
|
#endif
|
|
Deobfuscate(buffer, size, ref _obfuscationOffset);
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Open the next volume based on the current index
|
|
/// </summary>
|
|
private bool OpenNextVolume(out ushort nextVolume)
|
|
{
|
|
nextVolume = (ushort)(_volumeId + 1);
|
|
return OpenVolume(nextVolume);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Open the volume at the inputted index
|
|
/// </summary>
|
|
private bool OpenVolume(ushort volume)
|
|
{
|
|
// Read the volume from the cabinet set
|
|
var next = _cabinet.OpenVolume(volume, out var volumeStream);
|
|
if (next?.VolumeHeader is null || volumeStream is null)
|
|
{
|
|
Console.Error.WriteLine($"Failed to open input cabinet file {volume}");
|
|
return false;
|
|
}
|
|
|
|
// Assign the next items
|
|
_volumeFile?.Close();
|
|
_volumeFile = volumeStream;
|
|
_volumeHeader = next.VolumeHeader;
|
|
|
|
// Enable support for split archives for IS5
|
|
if (_cabinet.MajorVersion == 5)
|
|
{
|
|
if (_index < (_cabinet.FileCount - 1)
|
|
&& _index == _volumeHeader.LastFileIndex
|
|
&& _volumeHeader.LastFileSizeCompressed != _fileDescriptor.CompressedSize)
|
|
{
|
|
_fileDescriptor.Flags |= FileFlags.FILE_SPLIT;
|
|
}
|
|
else if (_index > 0
|
|
&& _index == _volumeHeader.FirstFileIndex
|
|
&& _volumeHeader.FirstFileSizeCompressed != _fileDescriptor.CompressedSize)
|
|
{
|
|
_fileDescriptor.Flags |= FileFlags.FILE_SPLIT;
|
|
}
|
|
}
|
|
|
|
ulong volumeBytesLeftCompressed, volumeBytesLeftExpanded;
|
|
#if NET20 || NET35
|
|
if ((_fileDescriptor.Flags & FileFlags.FILE_SPLIT) != 0)
|
|
#else
|
|
if (_fileDescriptor.Flags.HasFlag(FileFlags.FILE_SPLIT))
|
|
#endif
|
|
{
|
|
if (_index == _volumeHeader.LastFileIndex && _volumeHeader.LastFileOffset != 0x7FFFFFFF)
|
|
{
|
|
// can be first file too
|
|
_dataOffset = _volumeHeader.LastFileOffset;
|
|
volumeBytesLeftExpanded = _volumeHeader.LastFileSizeExpanded;
|
|
volumeBytesLeftCompressed = _volumeHeader.LastFileSizeCompressed;
|
|
}
|
|
else if (_index == _volumeHeader.FirstFileIndex)
|
|
{
|
|
_dataOffset = _volumeHeader.FirstFileOffset;
|
|
volumeBytesLeftExpanded = _volumeHeader.FirstFileSizeExpanded;
|
|
volumeBytesLeftCompressed = _volumeHeader.FirstFileSizeCompressed;
|
|
}
|
|
else
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_dataOffset = _fileDescriptor.DataOffset;
|
|
volumeBytesLeftExpanded = _fileDescriptor.ExpandedSize;
|
|
volumeBytesLeftCompressed = _fileDescriptor.CompressedSize;
|
|
}
|
|
|
|
#if NET20 || NET35
|
|
if ((_fileDescriptor.Flags & FileFlags.FILE_COMPRESSED) != 0)
|
|
#else
|
|
if (_fileDescriptor.Flags.HasFlag(FileFlags.FILE_COMPRESSED))
|
|
#endif
|
|
_volumeBytesLeft = volumeBytesLeftCompressed;
|
|
else
|
|
_volumeBytesLeft = volumeBytesLeftExpanded;
|
|
|
|
_volumeFile.SeekIfPossible((long)_dataOffset, SeekOrigin.Begin);
|
|
_volumeId = volume;
|
|
|
|
return true;
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|